use rustial_math::GeoCoord;
#[derive(Debug, Clone)]
pub struct GeoGrid {
pub origin: GeoCoord,
pub rows: usize,
pub cols: usize,
pub cell_width: f64,
pub cell_height: f64,
pub rotation: f64,
pub altitude_mode: crate::models::AltitudeMode,
}
const METERS_PER_DEG_LAT: f64 = 111_320.0;
impl GeoGrid {
pub fn new(
origin: GeoCoord,
rows: usize,
cols: usize,
cell_width: f64,
cell_height: f64,
) -> Self {
Self {
origin,
rows,
cols,
cell_width,
cell_height,
rotation: 0.0,
altitude_mode: crate::models::AltitudeMode::ClampToGround,
}
}
#[inline]
pub fn cell_count(&self) -> usize {
self.rows * self.cols
}
pub fn cell_center(&self, row: usize, col: usize) -> Option<GeoCoord> {
if row >= self.rows || col >= self.cols {
return None;
}
let dx = (col as f64 + 0.5) * self.cell_width;
let dy = (row as f64 + 0.5) * self.cell_height;
let (sin_r, cos_r) = self.rotation.sin_cos();
let rx = dx * cos_r - dy * sin_r;
let ry = dx * sin_r + dy * cos_r;
Some(offset_geo(&self.origin, rx, ry))
}
pub fn geo_bounds(&self) -> (GeoCoord, GeoCoord) {
let total_dx = self.cols as f64 * self.cell_width;
let total_dy = self.rows as f64 * self.cell_height;
if self.rotation.abs() < 1e-12 {
let se = offset_geo(&self.origin, total_dx, total_dy);
return (self.origin, se);
}
let corners = [
(0.0, 0.0),
(total_dx, 0.0),
(0.0, total_dy),
(total_dx, total_dy),
];
let (sin_r, cos_r) = self.rotation.sin_cos();
let mut min_lat = f64::MAX;
let mut max_lat = f64::MIN;
let mut min_lon = f64::MAX;
let mut max_lon = f64::MIN;
for &(dx, dy) in &corners {
let rx = dx * cos_r - dy * sin_r;
let ry = dx * sin_r + dy * cos_r;
let c = offset_geo(&self.origin, rx, ry);
min_lat = min_lat.min(c.lat);
max_lat = max_lat.max(c.lat);
min_lon = min_lon.min(c.lon);
max_lon = max_lon.max(c.lon);
}
(
GeoCoord::from_lat_lon(max_lat, min_lon),
GeoCoord::from_lat_lon(min_lat, max_lon),
)
}
pub fn cell_at_geo(&self, coord: &GeoCoord) -> Option<(usize, usize)> {
let (dx, dy) = geo_offset(&self.origin, coord);
let (sin_r, cos_r) = self.rotation.sin_cos();
let ux = dx * cos_r + dy * sin_r;
let uy = -dx * sin_r + dy * cos_r;
if ux < 0.0 || uy < 0.0 {
return None;
}
let col = (ux / self.cell_width) as usize;
let row = (uy / self.cell_height) as usize;
if row < self.rows && col < self.cols {
Some((row, col))
} else {
None
}
}
}
fn offset_geo(origin: &GeoCoord, dx_meters: f64, dy_meters: f64) -> GeoCoord {
let lat = origin.lat - dy_meters / METERS_PER_DEG_LAT;
let cos_lat = origin.lat.to_radians().cos().max(1e-10);
let lon = origin.lon + dx_meters / (METERS_PER_DEG_LAT * cos_lat);
GeoCoord::from_lat_lon(lat, lon)
}
fn geo_offset(origin: &GeoCoord, coord: &GeoCoord) -> (f64, f64) {
let cos_lat = origin.lat.to_radians().cos().max(1e-10);
let dx = (coord.lon - origin.lon) * METERS_PER_DEG_LAT * cos_lat;
let dy = (origin.lat - coord.lat) * METERS_PER_DEG_LAT;
(dx, dy)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cell_center_round_trips_with_cell_at_geo() {
let grid = GeoGrid::new(GeoCoord::from_lat_lon(51.1, 17.0), 10, 10, 100.0, 100.0);
for row in 0..grid.rows {
for col in 0..grid.cols {
let center = grid.cell_center(row, col).unwrap();
let (r, c) = grid.cell_at_geo(¢er).unwrap();
assert_eq!((r, c), (row, col), "round-trip failed for ({row}, {col})");
}
}
}
#[test]
fn cell_center_out_of_bounds() {
let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 5, 5, 50.0, 50.0);
assert!(grid.cell_center(5, 0).is_none());
assert!(grid.cell_center(0, 5).is_none());
}
#[test]
fn cell_at_geo_outside_grid() {
let grid = GeoGrid::new(GeoCoord::from_lat_lon(51.1, 17.0), 5, 5, 100.0, 100.0);
assert!(grid
.cell_at_geo(&GeoCoord::from_lat_lon(40.0, 17.0))
.is_none());
assert!(grid
.cell_at_geo(&GeoCoord::from_lat_lon(51.1, 16.0))
.is_none());
}
#[test]
fn geo_bounds_no_rotation() {
let grid = GeoGrid::new(GeoCoord::from_lat_lon(51.1, 17.0), 10, 10, 100.0, 100.0);
let (nw, se) = grid.geo_bounds();
assert!((nw.lat - 51.1).abs() < 1e-6);
assert!((nw.lon - 17.0).abs() < 1e-6);
assert!(se.lat < nw.lat);
assert!(se.lon > nw.lon);
}
#[test]
fn cell_count() {
let grid = GeoGrid::new(GeoCoord::from_lat_lon(0.0, 0.0), 3, 7, 10.0, 10.0);
assert_eq!(grid.cell_count(), 21);
}
#[test]
fn geo_bounds_at_high_latitude() {
let grid = GeoGrid::new(GeoCoord::from_lat_lon(70.0, 25.0), 5, 5, 200.0, 200.0);
let (nw, se) = grid.geo_bounds();
assert!(se.lat < nw.lat);
assert!(se.lon > nw.lon);
}
}