solverforge_maps/routing/
bbox.rs

1//! Bounding box for geographic queries.
2
3use serde::{Deserialize, Serialize};
4
5use super::coord::Coord;
6use super::error::BBoxError;
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
9pub struct BoundingBox {
10    pub min_lat: f64,
11    pub min_lng: f64,
12    pub max_lat: f64,
13    pub max_lng: f64,
14}
15
16impl BoundingBox {
17    /// Creates a new bounding box, panicking on invalid input.
18    ///
19    /// # Panics
20    ///
21    /// Panics if:
22    /// - Any value is NaN or infinite
23    /// - `min_lat > max_lat`
24    /// - `min_lng > max_lng`
25    /// - Latitude values are outside [-90, 90]
26    /// - Longitude values are outside [-180, 180]
27    pub fn new(min_lat: f64, min_lng: f64, max_lat: f64, max_lng: f64) -> Self {
28        match Self::try_new(min_lat, min_lng, max_lat, max_lng) {
29            Ok(bbox) => bbox,
30            Err(e) => panic!("invalid bounding box: {}", e),
31        }
32    }
33
34    pub fn try_new(
35        min_lat: f64,
36        min_lng: f64,
37        max_lat: f64,
38        max_lng: f64,
39    ) -> Result<Self, BBoxError> {
40        // Check NaN
41        if min_lat.is_nan() {
42            return Err(BBoxError::NaN { field: "min_lat" });
43        }
44        if min_lng.is_nan() {
45            return Err(BBoxError::NaN { field: "min_lng" });
46        }
47        if max_lat.is_nan() {
48            return Err(BBoxError::NaN { field: "max_lat" });
49        }
50        if max_lng.is_nan() {
51            return Err(BBoxError::NaN { field: "max_lng" });
52        }
53
54        // Check infinite
55        if min_lat.is_infinite() {
56            return Err(BBoxError::Infinite {
57                field: "min_lat",
58                value: min_lat,
59            });
60        }
61        if min_lng.is_infinite() {
62            return Err(BBoxError::Infinite {
63                field: "min_lng",
64                value: min_lng,
65            });
66        }
67        if max_lat.is_infinite() {
68            return Err(BBoxError::Infinite {
69                field: "max_lat",
70                value: max_lat,
71            });
72        }
73        if max_lng.is_infinite() {
74            return Err(BBoxError::Infinite {
75                field: "max_lng",
76                value: max_lng,
77            });
78        }
79
80        // Check latitude range
81        if !(-90.0..=90.0).contains(&min_lat) {
82            return Err(BBoxError::LatOutOfRange { value: min_lat });
83        }
84        if !(-90.0..=90.0).contains(&max_lat) {
85            return Err(BBoxError::LatOutOfRange { value: max_lat });
86        }
87
88        // Check longitude range
89        if !(-180.0..=180.0).contains(&min_lng) {
90            return Err(BBoxError::LngOutOfRange { value: min_lng });
91        }
92        if !(-180.0..=180.0).contains(&max_lng) {
93            return Err(BBoxError::LngOutOfRange { value: max_lng });
94        }
95
96        // Check min < max
97        if min_lat > max_lat {
98            return Err(BBoxError::MinLatGreaterThanMax {
99                min: min_lat,
100                max: max_lat,
101            });
102        }
103        if min_lng > max_lng {
104            return Err(BBoxError::MinLngGreaterThanMax {
105                min: min_lng,
106                max: max_lng,
107            });
108        }
109
110        Ok(Self {
111            min_lat,
112            min_lng,
113            max_lat,
114            max_lng,
115        })
116    }
117
118    pub fn from_coords(coords: &[Coord]) -> Self {
119        assert!(
120            !coords.is_empty(),
121            "Cannot create BoundingBox from empty coords"
122        );
123
124        let mut min_lat = f64::MAX;
125        let mut max_lat = f64::MIN;
126        let mut min_lng = f64::MAX;
127        let mut max_lng = f64::MIN;
128
129        for coord in coords {
130            min_lat = min_lat.min(coord.lat);
131            max_lat = max_lat.max(coord.lat);
132            min_lng = min_lng.min(coord.lng);
133            max_lng = max_lng.max(coord.lng);
134        }
135
136        Self {
137            min_lat,
138            min_lng,
139            max_lat,
140            max_lng,
141        }
142    }
143
144    pub fn expand(self, factor: f64) -> Self {
145        let lat_range = self.max_lat - self.min_lat;
146        let lng_range = self.max_lng - self.min_lng;
147        let lat_pad = lat_range * factor;
148        let lng_pad = lng_range * factor;
149
150        Self {
151            min_lat: self.min_lat - lat_pad,
152            min_lng: self.min_lng - lng_pad,
153            max_lat: self.max_lat + lat_pad,
154            max_lng: self.max_lng + lng_pad,
155        }
156    }
157
158    pub fn expand_meters(self, meters: f64) -> Self {
159        let lat_deg = meters / 111_320.0;
160        let center_lat = (self.min_lat + self.max_lat) / 2.0;
161        let lng_deg = meters / (111_320.0 * center_lat.to_radians().cos());
162
163        Self {
164            min_lat: self.min_lat - lat_deg,
165            min_lng: self.min_lng - lng_deg,
166            max_lat: self.max_lat + lat_deg,
167            max_lng: self.max_lng + lng_deg,
168        }
169    }
170
171    pub fn expand_for_routing(self, locations: &[Coord]) -> Self {
172        const DETOUR_FACTOR: f64 = 1.4;
173
174        if locations.len() < 2 {
175            return self;
176        }
177
178        let mut max_distance: f64 = 0.0;
179        for i in 0..locations.len() {
180            for j in (i + 1)..locations.len() {
181                let dist = locations[i].distance_to(locations[j]);
182                max_distance = max_distance.max(dist);
183            }
184        }
185
186        let expansion = max_distance * (DETOUR_FACTOR - 1.0) / 2.0;
187        self.expand_meters(expansion)
188    }
189
190    pub fn center(&self) -> Coord {
191        Coord::new(
192            (self.min_lat + self.max_lat) / 2.0,
193            (self.min_lng + self.max_lng) / 2.0,
194        )
195    }
196
197    pub fn contains(&self, coord: Coord) -> bool {
198        coord.lat >= self.min_lat
199            && coord.lat <= self.max_lat
200            && coord.lng >= self.min_lng
201            && coord.lng <= self.max_lng
202    }
203
204    pub(crate) fn cache_key(&self) -> String {
205        format!(
206            "{:.4}_{:.4}_{:.4}_{:.4}",
207            self.min_lat, self.min_lng, self.max_lat, self.max_lng
208        )
209    }
210}