use bevy_ecs::component::Component;
use bevy_ecs::prelude::*;
use bevy_reflect::Reflect;
use bevy_transform::components::GlobalTransform;
use bevy_math::{DVec3, Vec3};
use a5::{
cell_area, cell_to_boundary, cell_to_children, cell_to_lonlat, cell_to_parent,
get_resolution, lonlat_to_cell, LonLat, WORLD_CELL,
};
use crate::coord;
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
#[reflect(Component)]
pub struct GeoCell {
cell: u64,
}
impl GeoCell {
pub fn new(cell: u64) -> Self {
Self { cell }
}
pub fn from_lon_lat(longitude: f64, latitude: f64, resolution: i32) -> Option<Self> {
let lonlat = LonLat::new(longitude, latitude);
lonlat_to_cell(lonlat, resolution).ok().map(Self::new)
}
pub fn from_world_pos(pos: DVec3, resolution: i32) -> Option<Self> {
coord::world_pos_to_cell(pos, resolution).map(Self::new)
}
pub fn from_world_pos_f32(pos: Vec3, resolution: i32) -> Option<Self> {
Self::from_world_pos(pos.as_dvec3(), resolution)
}
pub fn raw(&self) -> u64 {
self.cell
}
pub fn center(&self) -> Option<LonLat> {
cell_to_lonlat(self.cell).ok()
}
pub fn resolution(&self) -> i32 {
get_resolution(self.cell)
}
pub fn parent(&self, resolution: i32) -> Option<Self> {
cell_to_parent(self.cell, Some(resolution))
.ok()
.map(Self::new)
}
pub fn parent_immediate(&self) -> Option<Self> {
cell_to_parent(self.cell, None).ok().map(Self::new)
}
pub fn children(&self, resolution: i32) -> Option<Vec<Self>> {
cell_to_children(self.cell, Some(resolution))
.ok()
.map(|cells| cells.into_iter().map(Self::new).collect())
}
pub fn children_immediate(&self) -> Option<Vec<Self>> {
cell_to_children(self.cell, None)
.ok()
.map(|cells| cells.into_iter().map(Self::new).collect())
}
pub fn boundary(&self) -> Option<Vec<LonLat>> {
cell_to_boundary(self.cell, None).ok()
}
pub fn area(&self) -> f64 {
cell_area(self.resolution())
}
pub fn is_world_cell(&self) -> bool {
self.cell == WORLD_CELL
}
}
impl Default for GeoCell {
fn default() -> Self {
Self { cell: WORLD_CELL }
}
}
impl From<u64> for GeoCell {
fn from(cell: u64) -> Self {
Self::new(cell)
}
}
impl From<GeoCell> for u64 {
fn from(cell: GeoCell) -> Self {
cell.cell
}
}
impl core::fmt::Display for GeoCell {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "A5Cell(0x{:x}, res={})", self.cell, self.resolution())
}
}
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component)]
pub struct CellTracker {
pub resolution: i32,
last_resolved_at: Option<Vec3>,
}
impl CellTracker {
pub fn new(resolution: i32) -> Self {
Self {
resolution,
last_resolved_at: None,
}
}
pub fn invalidate(&mut self) {
self.last_resolved_at = None;
}
}
impl Default for CellTracker {
fn default() -> Self {
Self::new(9)
}
}
pub fn update_geo_cells_from_world_pos(
mut q: Query<(&GlobalTransform, &mut GeoCell, &mut CellTracker)>,
) {
for (gt, mut cell, mut tracker) in q.iter_mut() {
let world = gt.translation();
let half_edge = (a5::cell_area(tracker.resolution) as f32).sqrt() * 0.5;
let needs_resolve = match tracker.last_resolved_at {
None => true,
Some(last) => (world - last).length_squared() > half_edge * half_edge,
};
if !needs_resolve {
continue;
}
let Some(new_cell) = GeoCell::from_world_pos_f32(world, tracker.resolution) else {
continue;
};
cell.set_if_neq(new_cell);
tracker.last_resolved_at = Some(world);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cell_tracker_skips_resolve_within_half_edge() {
use bevy_ecs::schedule::Schedule;
use bevy_ecs::world::World;
use bevy_transform::components::{GlobalTransform, Transform};
let mut world = World::new();
let resolution = 9;
let half_edge = (a5::cell_area(resolution) as f32).sqrt() * 0.5;
let initial_pos = Vec3::new(6_400_000.0, 0.0, 0.0);
let initial_cell = GeoCell::from_world_pos_f32(initial_pos, resolution).unwrap();
let mut tracker = CellTracker::new(resolution);
tracker.last_resolved_at = Some(initial_pos);
let nudge = Vec3::new(half_edge * 0.4, 0.0, 0.0);
let entity = world
.spawn((
initial_cell,
tracker,
GlobalTransform::from(Transform::from_translation(initial_pos + nudge)),
))
.id();
let mut schedule = Schedule::default();
schedule.add_systems(update_geo_cells_from_world_pos);
schedule.run(&mut world);
let tracker_after = world.get::<CellTracker>(entity).unwrap();
assert_eq!(
tracker_after.last_resolved_at,
Some(initial_pos),
"tracker should not have re-resolved after a sub-half-edge nudge"
);
let cell_after = *world.get::<GeoCell>(entity).unwrap();
assert_eq!(cell_after, initial_cell);
}
#[test]
fn cell_tracker_resolves_on_first_pass() {
use bevy_ecs::schedule::Schedule;
use bevy_ecs::world::World;
use bevy_transform::components::{GlobalTransform, Transform};
let mut world = World::new();
let pos = Vec3::new(6_400_000.0, 0.0, 0.0);
let entity = world
.spawn((
GeoCell::default(),
CellTracker::new(7),
GlobalTransform::from(Transform::from_translation(pos)),
))
.id();
let mut schedule = Schedule::default();
schedule.add_systems(update_geo_cells_from_world_pos);
schedule.run(&mut world);
let tracker_after = world.get::<CellTracker>(entity).unwrap();
assert!(tracker_after.last_resolved_at.is_some());
let cell_after = *world.get::<GeoCell>(entity).unwrap();
assert_eq!(cell_after.resolution(), 7);
let expected = GeoCell::from_world_pos_f32(pos, 7).unwrap();
assert_eq!(cell_after, expected);
}
#[test]
fn children_returns_uniform_resolution() {
let world = GeoCell::new(WORLD_CELL);
for res in [3, 5, 7] {
let kids = world.children(res).expect("children should succeed");
assert!(!kids.is_empty(), "no children at res {}", res);
for c in &kids {
assert_eq!(c.resolution(), res);
}
}
}
}