rustial_engine/visualization/
geo_grid.rs1use rustial_math::GeoCoord;
4
5#[derive(Debug, Clone)]
11pub struct GeoGrid {
12 pub origin: GeoCoord,
14 pub rows: usize,
16 pub cols: usize,
18 pub cell_width: f64,
20 pub cell_height: f64,
22 pub rotation: f64,
24 pub altitude_mode: crate::models::AltitudeMode,
26}
27
28const METERS_PER_DEG_LAT: f64 = 111_320.0;
30
31impl GeoGrid {
32 pub fn new(
37 origin: GeoCoord,
38 rows: usize,
39 cols: usize,
40 cell_width: f64,
41 cell_height: f64,
42 ) -> Self {
43 Self {
44 origin,
45 rows,
46 cols,
47 cell_width,
48 cell_height,
49 rotation: 0.0,
50 altitude_mode: crate::models::AltitudeMode::ClampToGround,
51 }
52 }
53
54 #[inline]
56 pub fn cell_count(&self) -> usize {
57 self.rows * self.cols
58 }
59
60 pub fn cell_center(&self, row: usize, col: usize) -> Option<GeoCoord> {
64 if row >= self.rows || col >= self.cols {
65 return None;
66 }
67 let dx = (col as f64 + 0.5) * self.cell_width;
69 let dy = (row as f64 + 0.5) * self.cell_height;
70
71 let (sin_r, cos_r) = self.rotation.sin_cos();
73 let rx = dx * cos_r - dy * sin_r;
74 let ry = dx * sin_r + dy * cos_r;
75
76 Some(offset_geo(&self.origin, rx, ry))
77 }
78
79 pub fn geo_bounds(&self) -> (GeoCoord, GeoCoord) {
81 let total_dx = self.cols as f64 * self.cell_width;
82 let total_dy = self.rows as f64 * self.cell_height;
83
84 if self.rotation.abs() < 1e-12 {
85 let se = offset_geo(&self.origin, total_dx, total_dy);
86 return (self.origin, se);
87 }
88
89 let corners = [
91 (0.0, 0.0),
92 (total_dx, 0.0),
93 (0.0, total_dy),
94 (total_dx, total_dy),
95 ];
96 let (sin_r, cos_r) = self.rotation.sin_cos();
97 let mut min_lat = f64::MAX;
98 let mut max_lat = f64::MIN;
99 let mut min_lon = f64::MAX;
100 let mut max_lon = f64::MIN;
101 for &(dx, dy) in &corners {
102 let rx = dx * cos_r - dy * sin_r;
103 let ry = dx * sin_r + dy * cos_r;
104 let c = offset_geo(&self.origin, rx, ry);
105 min_lat = min_lat.min(c.lat);
106 max_lat = max_lat.max(c.lat);
107 min_lon = min_lon.min(c.lon);
108 max_lon = max_lon.max(c.lon);
109 }
110 (
111 GeoCoord::from_lat_lon(max_lat, min_lon),
112 GeoCoord::from_lat_lon(min_lat, max_lon),
113 )
114 }
115
116 pub fn cell_at_geo(&self, coord: &GeoCoord) -> Option<(usize, usize)> {
120 let (dx, dy) = geo_offset(&self.origin, coord);
122
123 let (sin_r, cos_r) = self.rotation.sin_cos();
125 let ux = dx * cos_r + dy * sin_r;
126 let uy = -dx * sin_r + dy * cos_r;
127
128 if ux < 0.0 || uy < 0.0 {
129 return None;
130 }
131
132 let col = (ux / self.cell_width) as usize;
133 let row = (uy / self.cell_height) as usize;
134
135 if row < self.rows && col < self.cols {
136 Some((row, col))
137 } else {
138 None
139 }
140 }
141}
142
143fn offset_geo(origin: &GeoCoord, dx_meters: f64, dy_meters: f64) -> GeoCoord {
145 let lat = origin.lat - dy_meters / METERS_PER_DEG_LAT;
146 let cos_lat = origin.lat.to_radians().cos().max(1e-10);
147 let lon = origin.lon + dx_meters / (METERS_PER_DEG_LAT * cos_lat);
148 GeoCoord::from_lat_lon(lat, lon)
149}
150
151fn geo_offset(origin: &GeoCoord, coord: &GeoCoord) -> (f64, f64) {
153 let cos_lat = origin.lat.to_radians().cos().max(1e-10);
154 let dx = (coord.lon - origin.lon) * METERS_PER_DEG_LAT * cos_lat;
155 let dy = (origin.lat - coord.lat) * METERS_PER_DEG_LAT;
156 (dx, dy)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn cell_center_round_trips_with_cell_at_geo() {
165 let grid = GeoGrid::new(GeoCoord::from_lat_lon(51.1, 17.0), 10, 10, 100.0, 100.0);
166 for row in 0..grid.rows {
167 for col in 0..grid.cols {
168 let center = grid.cell_center(row, col).unwrap();
169 let (r, c) = grid.cell_at_geo(¢er).unwrap();
170 assert_eq!((r, c), (row, col), "round-trip failed for ({row}, {col})");
171 }
172 }
173 }
174
175 #[test]
176 fn cell_center_out_of_bounds() {
177 let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 5, 5, 50.0, 50.0);
178 assert!(grid.cell_center(5, 0).is_none());
179 assert!(grid.cell_center(0, 5).is_none());
180 }
181
182 #[test]
183 fn cell_at_geo_outside_grid() {
184 let grid = GeoGrid::new(GeoCoord::from_lat_lon(51.1, 17.0), 5, 5, 100.0, 100.0);
185 assert!(grid
187 .cell_at_geo(&GeoCoord::from_lat_lon(40.0, 17.0))
188 .is_none());
189 assert!(grid
191 .cell_at_geo(&GeoCoord::from_lat_lon(51.1, 16.0))
192 .is_none());
193 }
194
195 #[test]
196 fn geo_bounds_no_rotation() {
197 let grid = GeoGrid::new(GeoCoord::from_lat_lon(51.1, 17.0), 10, 10, 100.0, 100.0);
198 let (nw, se) = grid.geo_bounds();
199 assert!((nw.lat - 51.1).abs() < 1e-6);
200 assert!((nw.lon - 17.0).abs() < 1e-6);
201 assert!(se.lat < nw.lat);
202 assert!(se.lon > nw.lon);
203 }
204
205 #[test]
206 fn cell_count() {
207 let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 3, 7, 10.0, 10.0);
208 assert_eq!(grid.cell_count(), 21);
209 }
210
211 #[test]
212 fn geo_bounds_at_high_latitude() {
213 let grid = GeoGrid::new(GeoCoord::from_lat_lon(70.0, 25.0), 5, 5, 200.0, 200.0);
214 let (nw, se) = grid.geo_bounds();
215 assert!(se.lat < nw.lat);
216 assert!(se.lon > nw.lon);
217 }
218}