use rustial_math::TileId;
use std::collections::{HashMap, HashSet};
const EXTENT: f64 = 8192.0;
const ROUNDING_FACTOR: f64 = 512.0 / EXTENT / 2.0;
#[derive(Debug, Clone, Default)]
struct CrossTileIdGenerator {
next_id: u64,
}
impl CrossTileIdGenerator {
fn generate(&mut self) -> u64 {
self.next_id += 1;
self.next_id
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct SymbolKey {
source_layer: String,
text: String,
icon: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct GridPosition {
x: i32,
y: i32,
}
#[derive(Debug, Clone)]
struct TileSymbolGrid {
tile_id: TileId,
instance_id: u64,
entries: HashMap<SymbolKey, Vec<(GridPosition, u64)>>,
}
impl TileSymbolGrid {
fn new(tile_id: TileId, instance_id: u64) -> Self {
Self {
tile_id,
instance_id,
entries: HashMap::new(),
}
}
fn find_match(
&self,
key: &SymbolKey,
scaled_pos: GridPosition,
tolerance: i32,
used_ids: &HashSet<u64>,
) -> Option<u64> {
let bucket = self.entries.get(key)?;
for &(pos, cross_tile_id) in bucket {
if (pos.x - scaled_pos.x).abs() <= tolerance
&& (pos.y - scaled_pos.y).abs() <= tolerance
&& !used_ids.contains(&cross_tile_id)
{
return Some(cross_tile_id);
}
}
None
}
fn all_cross_tile_ids(&self) -> Vec<u64> {
self.entries
.values()
.flat_map(|bucket| bucket.iter().map(|&(_, id)| id))
.collect()
}
}
fn scaled_coordinates(
anchor_x: f64,
anchor_y: f64,
candidate_tile: TileId,
target_tile: TileId,
) -> GridPosition {
let z_diff = candidate_tile.zoom as i32 - target_tile.zoom as i32;
let scale = ROUNDING_FACTOR / 2.0_f64.powi(z_diff);
let x_world = (candidate_tile.x as f64 * EXTENT + anchor_x) * scale;
let y_world = (candidate_tile.y as f64 * EXTENT + anchor_y) * scale;
let x_offset = target_tile.x as f64 * EXTENT * ROUNDING_FACTOR;
let y_offset = target_tile.y as f64 * EXTENT * ROUNDING_FACTOR;
GridPosition {
x: (x_world - x_offset).floor() as i32,
y: (y_world - y_offset).floor() as i32,
}
}
fn native_grid_position(anchor_x: f64, anchor_y: f64) -> GridPosition {
GridPosition {
x: (anchor_x * ROUNDING_FACTOR).floor() as i32,
y: (anchor_y * ROUNDING_FACTOR).floor() as i32,
}
}
#[derive(Debug, Clone, Default)]
struct LayerCrossTileIndex {
grids: HashMap<u8, HashMap<u64, TileSymbolGrid>>,
used_ids: HashMap<u8, HashSet<u64>>,
}
fn tile_key(tile: &TileId) -> u64 {
((tile.zoom as u64) << 48) | ((tile.x as u64) << 24) | (tile.y as u64)
}
fn is_child_of(child: &TileId, parent: &TileId) -> bool {
if child.zoom <= parent.zoom {
return false;
}
let dz = child.zoom - parent.zoom;
(child.x >> dz) == parent.x && (child.y >> dz) == parent.y
}
fn scaled_to(tile: &TileId, target_zoom: u8) -> TileId {
if target_zoom >= tile.zoom {
return *tile;
}
let dz = tile.zoom - target_zoom;
TileId::new(target_zoom, tile.x >> dz, tile.y >> dz)
}
impl LayerCrossTileIndex {
fn add_tile(
&mut self,
tile_id: TileId,
instance_id: u64,
candidates: &mut [SymbolCandidateEntry],
id_gen: &mut CrossTileIdGenerator,
) -> bool {
let tk = tile_key(&tile_id);
if let Some(zoom_grids) = self.grids.get(&tile_id.zoom) {
if let Some(existing) = zoom_grids.get(&tk) {
if existing.instance_id == instance_id {
return false;
}
}
}
if let Some(zoom_grids) = self.grids.get(&tile_id.zoom) {
if let Some(old_grid) = zoom_grids.get(&tk) {
let old_ids = old_grid.all_cross_tile_ids();
if let Some(used) = self.used_ids.get_mut(&tile_id.zoom) {
for id in &old_ids {
used.remove(id);
}
}
}
}
for c in candidates.iter_mut() {
c.assigned_cross_tile_id = 0;
}
let zoom_used = self.used_ids.entry(tile_id.zoom).or_default();
for (&z, zoom_grids) in &self.grids {
if z > tile_id.zoom {
for grid in zoom_grids.values() {
if is_child_of(&grid.tile_id, &tile_id) {
Self::match_candidates(candidates, grid, &tile_id, zoom_used);
}
}
} else {
let parent_coord = scaled_to(&tile_id, z);
let parent_key = tile_key(&parent_coord);
if let Some(parent_grid) = zoom_grids.get(&parent_key) {
Self::match_candidates(candidates, parent_grid, &tile_id, zoom_used);
}
}
}
for c in candidates.iter_mut() {
if c.assigned_cross_tile_id == 0 {
let id = id_gen.generate();
c.assigned_cross_tile_id = id;
zoom_used.insert(id);
}
}
let mut grid = TileSymbolGrid::new(tile_id, instance_id);
for c in candidates.iter() {
let key = c.key.clone();
let pos = native_grid_position(c.anchor_x, c.anchor_y);
grid.entries
.entry(key)
.or_default()
.push((pos, c.assigned_cross_tile_id));
}
self.grids.entry(tile_id.zoom).or_default().insert(tk, grid);
true
}
fn match_candidates(
candidates: &mut [SymbolCandidateEntry],
grid: &TileSymbolGrid,
candidate_tile: &TileId,
used_ids: &mut HashSet<u64>,
) {
let tolerance = if grid.tile_id.zoom < candidate_tile.zoom {
1
} else {
1i32 << (grid.tile_id.zoom as i32 - candidate_tile.zoom as i32).max(0)
};
for c in candidates.iter_mut() {
if c.assigned_cross_tile_id != 0 {
continue;
}
let scaled = scaled_coordinates(c.anchor_x, c.anchor_y, *candidate_tile, grid.tile_id);
if let Some(matched_id) = grid.find_match(&c.key, scaled, tolerance, used_ids) {
c.assigned_cross_tile_id = matched_id;
used_ids.insert(matched_id);
}
}
}
fn remove_stale(&mut self, current_ids: &HashSet<u64>) -> bool {
let mut changed = false;
for (&z, zoom_grids) in self.grids.iter_mut() {
let before = zoom_grids.len();
let used = self.used_ids.entry(z).or_default();
zoom_grids.retain(|_tk, grid| {
if current_ids.contains(&grid.instance_id) {
true
} else {
for id in grid.all_cross_tile_ids() {
used.remove(&id);
}
false
}
});
if zoom_grids.len() != before {
changed = true;
}
}
changed
}
fn remove_tile(&mut self, tile_id: &TileId) {
let tk = tile_key(tile_id);
if let Some(zoom_grids) = self.grids.get_mut(&tile_id.zoom) {
if let Some(grid) = zoom_grids.remove(&tk) {
if let Some(used) = self.used_ids.get_mut(&tile_id.zoom) {
for id in grid.all_cross_tile_ids() {
used.remove(&id);
}
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct SymbolCandidateEntry {
key: SymbolKey,
anchor_x: f64,
anchor_y: f64,
pub assigned_cross_tile_id: u64,
}
impl SymbolCandidateEntry {
pub fn new(source_layer: &str, text: &str, icon: &str, anchor_x: f64, anchor_y: f64) -> Self {
Self {
key: SymbolKey {
source_layer: source_layer.to_owned(),
text: text.to_owned(),
icon: icon.to_owned(),
},
anchor_x,
anchor_y,
assigned_cross_tile_id: 0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CrossTileSymbolIndex {
layers: HashMap<String, LayerCrossTileIndex>,
id_gen: CrossTileIdGenerator,
next_instance_id: u64,
}
impl CrossTileSymbolIndex {
pub fn new() -> Self {
Self::default()
}
pub fn next_instance_id(&mut self) -> u64 {
self.next_instance_id += 1;
self.next_instance_id
}
pub fn assign(
&mut self,
layer_id: &str,
tile_id: TileId,
instance_id: u64,
candidates: &mut [SymbolCandidateEntry],
) -> bool {
let layer_index = self.layers.entry(layer_id.to_owned()).or_default();
layer_index.add_tile(tile_id, instance_id, candidates, &mut self.id_gen)
}
pub fn prune_stale(&mut self, layer_id: &str, current_instance_ids: &HashSet<u64>) -> bool {
if let Some(layer_index) = self.layers.get_mut(layer_id) {
layer_index.remove_stale(current_instance_ids)
} else {
false
}
}
pub fn remove_tile(&mut self, tile_id: &TileId) {
for layer_index in self.layers.values_mut() {
layer_index.remove_tile(tile_id);
}
}
pub fn prune_layers(&mut self, active_layers: &HashSet<String>) {
self.layers.retain(|id, _| active_layers.contains(id));
}
pub fn tile_count(&self) -> usize {
self.layers
.values()
.flat_map(|layer| layer.grids.values())
.map(|zoom_grids| zoom_grids.len())
.sum()
}
pub fn entry_count(&self) -> usize {
self.layers
.values()
.flat_map(|layer| layer.grids.values())
.flat_map(|zoom_grids| zoom_grids.values())
.map(|grid| {
grid.entries
.values()
.map(|bucket| bucket.len())
.sum::<usize>()
})
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(source_layer: &str, text: &str, anchor_x: f64, anchor_y: f64) -> SymbolCandidateEntry {
SymbolCandidateEntry::new(source_layer, text, "", anchor_x, anchor_y)
}
#[test]
fn assigns_unique_ids_to_distinct_symbols() {
let mut index = CrossTileSymbolIndex::new();
let iid = index.next_instance_id();
let tile = TileId::new(10, 512, 512);
let mut candidates = vec![
entry("poi", "Restaurant", 100.0, 200.0),
entry("poi", "Cafe", 300.0, 400.0),
];
index.assign("symbols", tile, iid, &mut candidates);
assert_ne!(candidates[0].assigned_cross_tile_id, 0);
assert_ne!(candidates[1].assigned_cross_tile_id, 0);
assert_ne!(
candidates[0].assigned_cross_tile_id,
candidates[1].assigned_cross_tile_id
);
}
#[test]
fn matches_same_symbol_across_parent_and_child_tiles() {
let mut index = CrossTileSymbolIndex::new();
let parent = TileId::new(10, 512, 512);
let parent_iid = index.next_instance_id();
let mut parent_candidates = vec![entry("poi", "Museum", 2048.0, 2048.0)];
index.assign("symbols", parent, parent_iid, &mut parent_candidates);
let parent_id = parent_candidates[0].assigned_cross_tile_id;
assert_ne!(parent_id, 0);
let child = TileId::new(11, 1024, 1024);
let child_iid = index.next_instance_id();
let mut child_candidates = vec![entry("poi", "Museum", 4096.0, 4096.0)];
index.assign("symbols", child, child_iid, &mut child_candidates);
assert_eq!(child_candidates[0].assigned_cross_tile_id, parent_id);
}
#[test]
fn does_not_match_different_text() {
let mut index = CrossTileSymbolIndex::new();
let parent = TileId::new(10, 512, 512);
let parent_iid = index.next_instance_id();
let mut parent_candidates = vec![entry("poi", "Museum", 2048.0, 2048.0)];
index.assign("symbols", parent, parent_iid, &mut parent_candidates);
let parent_id = parent_candidates[0].assigned_cross_tile_id;
let child = TileId::new(11, 1024, 1024);
let child_iid = index.next_instance_id();
let mut child_candidates = vec![entry("poi", "Library", 4096.0, 4096.0)];
index.assign("symbols", child, child_iid, &mut child_candidates);
assert_ne!(child_candidates[0].assigned_cross_tile_id, parent_id);
}
#[test]
fn eviction_removes_tile_entries() {
let mut index = CrossTileSymbolIndex::new();
let tile = TileId::new(10, 512, 512);
let iid = index.next_instance_id();
let mut candidates = vec![entry("poi", "Park", 1000.0, 2000.0)];
index.assign("symbols", tile, iid, &mut candidates);
assert_eq!(index.entry_count(), 1);
assert_eq!(index.tile_count(), 1);
index.remove_tile(&tile);
assert_eq!(index.entry_count(), 0);
assert_eq!(index.tile_count(), 0);
}
#[test]
fn prune_stale_removes_old_tiles() {
let mut index = CrossTileSymbolIndex::new();
let tile_a = TileId::new(10, 512, 512);
let iid_a = index.next_instance_id();
let mut a = vec![entry("poi", "A", 100.0, 100.0)];
index.assign("symbols", tile_a, iid_a, &mut a);
let tile_b = TileId::new(10, 513, 512);
let iid_b = index.next_instance_id();
let mut b = vec![entry("poi", "B", 100.0, 100.0)];
index.assign("symbols", tile_b, iid_b, &mut b);
assert_eq!(index.tile_count(), 2);
let mut active = HashSet::new();
active.insert(iid_a);
let changed = index.prune_stale("symbols", &active);
assert!(changed);
assert_eq!(index.tile_count(), 1);
}
#[test]
fn same_instance_id_is_no_op() {
let mut index = CrossTileSymbolIndex::new();
let tile = TileId::new(10, 512, 512);
let iid = index.next_instance_id();
let mut candidates = vec![entry("poi", "Park", 1000.0, 2000.0)];
let changed1 = index.assign("symbols", tile, iid, &mut candidates);
assert!(changed1);
let mut candidates2 = vec![entry("poi", "Park", 1000.0, 2000.0)];
let changed2 = index.assign("symbols", tile, iid, &mut candidates2);
assert!(!changed2);
}
#[test]
fn adjacent_tiles_at_same_zoom_do_not_cross_match() {
let mut index = CrossTileSymbolIndex::new();
let tile_a = TileId::new(10, 512, 512);
let iid_a = index.next_instance_id();
let mut a = vec![entry("poi", "Church", 7000.0, 4096.0)];
index.assign("symbols", tile_a, iid_a, &mut a);
let tile_b = TileId::new(10, 513, 512);
let iid_b = index.next_instance_id();
let mut b = vec![entry("poi", "Church", 100.0, 4096.0)];
index.assign("symbols", tile_b, iid_b, &mut b);
assert_ne!(a[0].assigned_cross_tile_id, b[0].assigned_cross_tile_id);
}
#[test]
fn child_registered_first_propagates_to_parent() {
let mut index = CrossTileSymbolIndex::new();
let child = TileId::new(11, 1024, 1024);
let child_iid = index.next_instance_id();
let mut child_candidates = vec![entry("poi", "Station", 4096.0, 4096.0)];
index.assign("symbols", child, child_iid, &mut child_candidates);
let child_id = child_candidates[0].assigned_cross_tile_id;
let parent = TileId::new(10, 512, 512);
let parent_iid = index.next_instance_id();
let mut parent_candidates = vec![entry("poi", "Station", 2048.0, 2048.0)];
index.assign("symbols", parent, parent_iid, &mut parent_candidates);
assert_eq!(parent_candidates[0].assigned_cross_tile_id, child_id);
}
#[test]
fn prune_layers_removes_unused_layers() {
let mut index = CrossTileSymbolIndex::new();
let tile = TileId::new(10, 512, 512);
let iid = index.next_instance_id();
let mut a = vec![entry("poi", "A", 100.0, 100.0)];
index.assign("layer-a", tile, iid, &mut a);
let iid2 = index.next_instance_id();
let mut b = vec![entry("road", "B", 200.0, 200.0)];
index.assign("layer-b", tile, iid2, &mut b);
assert_eq!(index.tile_count(), 2);
let mut active = HashSet::new();
active.insert("layer-a".to_owned());
index.prune_layers(&active);
assert_eq!(index.tile_count(), 1);
}
#[test]
fn replacement_tile_reclaims_ids() {
let mut index = CrossTileSymbolIndex::new();
let tile = TileId::new(10, 512, 512);
let iid1 = index.next_instance_id();
let mut a = vec![entry("poi", "Old", 100.0, 100.0)];
index.assign("symbols", tile, iid1, &mut a);
let old_id = a[0].assigned_cross_tile_id;
let iid2 = index.next_instance_id();
let mut b = vec![entry("poi", "New", 100.0, 100.0)];
index.assign("symbols", tile, iid2, &mut b);
assert_ne!(b[0].assigned_cross_tile_id, old_id);
assert_eq!(index.tile_count(), 1);
}
#[test]
fn multi_zoom_chain_propagates_ids() {
let mut index = CrossTileSymbolIndex::new();
let z9 = TileId::new(9, 256, 256);
let iid9 = index.next_instance_id();
let mut c9 = vec![entry("poi", "Bridge", 1024.0, 1024.0)];
index.assign("symbols", z9, iid9, &mut c9);
let id9 = c9[0].assigned_cross_tile_id;
let z10 = TileId::new(10, 512, 512);
let iid10 = index.next_instance_id();
let mut c10 = vec![entry("poi", "Bridge", 2048.0, 2048.0)];
index.assign("symbols", z10, iid10, &mut c10);
assert_eq!(c10[0].assigned_cross_tile_id, id9);
let z11 = TileId::new(11, 1024, 1024);
let iid11 = index.next_instance_id();
let mut c11 = vec![entry("poi", "Bridge", 4096.0, 4096.0)];
index.assign("symbols", z11, iid11, &mut c11);
assert_eq!(c11[0].assigned_cross_tile_id, id9);
}
}