use std::collections::HashMap;
use crate::coord::EnuConverter;
use crate::octree::OctreeNode;
use crate::store::ZoneStore;
use crate::zone::{Zone, ZoneEntry};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct TileKey {
pub zoom: u8,
pub tx: i32,
pub ty: i32,
}
impl TileKey {
pub fn deg_size(zoom: u8) -> f64 {
360.0 / (1u64 << zoom) as f64
}
pub fn from_geodetic(lat: f64, lon: f64, zoom: u8) -> Self {
let s = Self::deg_size(zoom);
Self {
zoom,
tx: ((lon + 180.0) / s).floor() as i32,
ty: ((lat + 90.0) / s).floor() as i32,
}
}
pub fn origin(&self) -> (f64, f64) {
let s = Self::deg_size(self.zoom);
let lat = self.ty as f64 * s - 90.0;
let lon = self.tx as f64 * s - 180.0;
(lat, lon)
}
pub fn neighbors(&self) -> [TileKey; 9] {
let mut out = [*self; 9];
let mut i = 0;
for dy in -1i32..=1 {
for dx in -1i32..=1 {
out[i] = TileKey { zoom: self.zoom, tx: self.tx + dx, ty: self.ty + dy };
i += 1;
}
}
out
}
}
pub struct WorldTile {
pub key: TileKey,
pub conv: EnuConverter,
pub store: ZoneStore,
pub octree: OctreeNode,
}
impl WorldTile {
pub fn new(key: TileKey) -> Self {
let (lat, lon) = key.origin();
let conv = EnuConverter::new(lat, lon, 0.0);
let store = ZoneStore::from_entries(&[], &conv);
Self {
key,
conv,
store,
octree: OctreeNode::new([0.0; 3], 50_000.0), }
}
}
pub struct TiledWorld {
pub zoom: u8,
pub tiles: HashMap<TileKey, WorldTile>,
}
impl TiledWorld {
pub fn new(zoom: u8) -> Self {
Self { zoom, tiles: HashMap::new() }
}
fn ensure_tile(&mut self, key: TileKey) {
self.tiles.entry(key).or_insert_with(|| WorldTile::new(key));
}
fn tile_for(&mut self, lat: f64, lon: f64) -> &mut WorldTile {
let key = TileKey::from_geodetic(lat, lon, self.zoom);
self.tiles.entry(key).or_insert_with(|| WorldTile::new(key))
}
pub fn insert_point(&mut self, lat: f64, lon: f64, alt: f64) {
let tile = self.tile_for(lat, lon);
let enu = tile.conv.to_enu(lat, lon, alt);
tile.octree.insert(enu, 8);
}
pub fn query_geodetic(&self, lat: f64, lon: f64, alt: f64) -> Vec<(TileKey, Vec<u32>)> {
let key = TileKey::from_geodetic(lat, lon, self.zoom);
let mut results = vec![];
for nk in key.neighbors() {
if let Some(tile) = self.tiles.get(&nk) {
let enu = tile.conv.to_enu(lat, lon, alt);
let hits = tile.store.query_enu(enu);
if !hits.is_empty() {
results.push((nk, hits.to_vec()));
}
}
}
results
}
pub fn add_zone(&mut self, entry: ZoneEntry) {
for key in self.tiles_for_zone(&entry.zone) {
self.ensure_tile(key);
let tile = self.tiles.get_mut(&key).unwrap();
let conv = tile.conv;
tile.store.add_zone(entry.id, &entry.zone, &conv);
}
}
fn tiles_for_zone(&self, zone: &Zone) -> Vec<TileKey> {
let (min_lat, min_lon, max_lat, max_lon) = zone_geodetic_bbox(zone);
let deg = TileKey::deg_size(self.zoom);
let tx0 = ((min_lon + 180.0) / deg).floor() as i32;
let ty0 = ((min_lat + 90.0) / deg).floor() as i32;
let tx1 = ((max_lon + 180.0) / deg).floor() as i32;
let ty1 = ((max_lat + 90.0) / deg).floor() as i32;
let mut keys = vec![];
for ty in ty0..=ty1 {
for tx in tx0..=tx1 {
keys.push(TileKey { zoom: self.zoom, tx, ty });
}
}
keys
}
}
pub fn zone_geodetic_bbox(zone: &Zone) -> (f64, f64, f64, f64) {
match zone {
Zone::Aabb { min, max } => (min[0], min[1], max[0], max[1]),
Zone::Cylinder { center, radius_m, .. } => {
let dlat = radius_m / 111_111.0;
let dlon = radius_m / (111_111.0 * center[0].to_radians().cos());
(center[0] - dlat, center[1] - dlon, center[0] + dlat, center[1] + dlon)
}
Zone::ExtrudedPolygon { ring, .. } => bbox_of(ring.iter().map(|v| (v[0], v[1]))),
Zone::ConvexHull { vertices } => bbox_of(vertices.iter().map(|v| (v[0], v[1]))),
}
}
fn bbox_of(coords: impl Iterator<Item = (f64, f64)>) -> (f64, f64, f64, f64) {
let (mut min_lat, mut min_lon, mut max_lat, mut max_lon) =
(f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for (lat, lon) in coords {
min_lat = min_lat.min(lat);
min_lon = min_lon.min(lon);
max_lat = max_lat.max(lat);
max_lon = max_lon.max(lon);
}
(min_lat, min_lon, max_lat, max_lon)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tile_origin_is_south_west_of_point() {
let zoom = 14;
let (lat, lon) = (10.7626, 106.6601);
let key = TileKey::from_geodetic(lat, lon, zoom);
let (olat, olon) = key.origin();
let s = TileKey::deg_size(zoom);
assert!(olat <= lat && lat < olat + s, "lat in tile: {olat}..{}", olat + s);
assert!(olon <= lon && lon < olon + s, "lon in tile: {olon}..{}", olon + s);
}
#[test]
fn neighbors_include_self_and_eight() {
let key = TileKey { zoom: 10, tx: 5, ty: 7 };
let n = key.neighbors();
assert_eq!(n.len(), 9);
assert!(n.contains(&key));
assert!(n.contains(&TileKey { zoom: 10, tx: 4, ty: 6 }));
assert!(n.contains(&TileKey { zoom: 10, tx: 6, ty: 8 }));
}
#[test]
fn add_zone_then_query_finds_it() {
let mut world = TiledWorld::new(14);
world.add_zone(ZoneEntry::new(
7,
Zone::Cylinder { center: [10.7626, 106.6601], radius_m: 40.0, z_min: 0.0, z_max: 20.0 },
));
let hits = world.query_geodetic(10.7626, 106.6601, 5.0);
let ids: Vec<u32> = hits.iter().flat_map(|(_, v)| v.iter().copied()).collect();
assert!(ids.contains(&7), "expected zone 7, got {hits:?}");
}
#[test]
fn query_empty_world_is_empty() {
let world = TiledWorld::new(12);
assert!(world.query_geodetic(0.0, 0.0, 0.0).is_empty());
}
#[test]
fn cylinder_bbox_brackets_center() {
let z = Zone::Cylinder { center: [48.0, 11.0], radius_m: 1000.0, z_min: 0.0, z_max: 1.0 };
let (min_lat, min_lon, max_lat, max_lon) = zone_geodetic_bbox(&z);
assert!(min_lat < 48.0 && 48.0 < max_lat);
assert!(min_lon < 11.0 && 11.0 < max_lon);
assert!((max_lat - min_lat - 0.018).abs() < 0.001, "lat span ≈ 0.018");
}
#[test]
fn insert_point_creates_tile() {
let mut world = TiledWorld::new(14);
assert_eq!(world.tiles.len(), 0);
world.insert_point(10.7626, 106.6601, 5.0);
assert_eq!(world.tiles.len(), 1);
}
}