solverforge_maps/routing/
bbox.rs1use 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 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 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 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 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 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 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}