use geo_types::Coord;
use crate::TileError;
use crate::clip_polyline::{segment_intersects, to_local};
use crate::tile::{TileId, tile_of};
use crate::vertex::Vertex;
pub(crate) const MAX_TILE_VISITS: i64 = 1 << 25;
#[inline]
const fn shift(c: Coord<i32>, d: i32) -> Coord<i32> {
Coord {
x: c.x + d,
y: c.y + d,
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "callers pass |n| <= d, so the quotient is within |m| and fits i64"
)]
fn mul_div_floor(n: i64, m: i64, d: i64) -> i64 {
match n.checked_mul(m) {
Some(p) => p.div_euclid(d),
None => (i128::from(n) * i128::from(m)).div_euclid(i128::from(d)) as i64,
}
}
pub(crate) trait RouteSink<V: Vertex> {
fn begin_polyline(&mut self);
fn begin_segment(&mut self);
fn emit(&mut self, tile: TileId, origin: Coord<i32>, a: V, c: V) -> Result<(), TileError>;
fn end_segment(
&mut self,
a: Coord<i32>,
c: Coord<i32>,
row: Option<i32>,
) -> Result<(), TileError> {
let _ = (a, c, row);
Ok(())
}
}
#[derive(Clone, Copy)]
struct Located {
owner: TileId,
core_lo: Coord<i32>,
core_hi: Coord<i32>,
inner_lo: Coord<i32>,
inner_hi: Coord<i32>,
}
impl Located {
fn contains_core(&self, c: Coord<i32>) -> bool {
c.x >= self.core_lo.x
&& c.x <= self.core_hi.x
&& c.y >= self.core_lo.y
&& c.y <= self.core_hi.y
}
fn contains_inner(&self, c: Coord<i32>) -> bool {
c.x >= self.inner_lo.x
&& c.x <= self.inner_hi.x
&& c.y >= self.inner_lo.y
&& c.y <= self.inner_hi.y
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Grid {
extent: i32,
buffer: i32,
}
impl Grid {
pub(crate) const fn new(extent: u32, buffer: u16) -> Result<Self, TileError> {
if extent == 0 || extent > i32::MAX.cast_unsigned() {
return Err(TileError::InvalidExtent);
}
if 2 * (buffer as u32) >= extent {
return Err(TileError::BufferTooLarge);
}
Ok(Self {
extent: extent.cast_signed(),
buffer: buffer as i32,
})
}
pub(crate) fn extent(self) -> u32 {
self.extent.cast_unsigned()
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "buffer is always in 0..=u16::MAX (it was built from a u16)"
)]
pub(crate) fn buffer(self) -> u16 {
self.buffer as u16
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub(crate) fn slice_one<V: Vertex>(
self,
polyline: &[V],
tile: TileId,
) -> Result<Vec<Vec<V>>, TileError> {
let poly = polyline;
let (min, max) = self.tile_buffered_bounds(tile)?;
let origin = shift(min, self.buffer);
let mut runs = Vec::new();
let mut cur: Vec<V> = Vec::new();
let mut prev: Option<V> = None;
let mut left_hit = false;
for &c in poly {
if let Some(a) = prev {
if a.position() == c.position() {
continue; }
let this_hit = segment_intersects(a.position(), c.position(), min, max);
if left_hit || this_hit {
cur.push(to_local(a, origin)?); } else if cur.len() >= 2 {
runs.push(std::mem::take(&mut cur)); } else {
cur.clear();
}
left_hit = this_hit;
}
prev = Some(c);
}
if left_hit && let Some(a) = prev {
cur.push(to_local(a, origin)?);
}
if cur.len() >= 2 {
runs.push(cur);
}
Ok(runs)
}
pub(crate) fn route<V: Vertex, S: RouteSink<V>>(
self,
polyline: &[V],
sink: &mut S,
) -> Result<(), TileError> {
u32::try_from(polyline.len()).map_err(|_| TileError::GeometryTooLarge)?;
let mut budget = MAX_TILE_VISITS;
self.route_within(polyline, sink, &mut budget)
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub(crate) fn route_within<V: Vertex, S: RouteSink<V>>(
self,
polyline: &[V],
sink: &mut S,
budget: &mut i64,
) -> Result<(), TileError> {
let poly = polyline;
if poly.is_empty() {
return Ok(());
}
sink.begin_polyline();
let mut prev: Option<V> = None;
let mut prev_loc: Option<Located> = None;
for v in poly {
let c = v.position();
if let Some(a) = prev {
let a_pos = a.position();
if a_pos == c {
continue; }
sink.begin_segment();
let la = match prev_loc {
Some(l) => l,
None => self.locate(a_pos)?,
};
if la.contains_inner(a_pos) && la.contains_inner(c) {
*budget -= 1;
if *budget < 0 {
return Err(TileError::TooManyTiles);
}
sink.emit(la.owner, la.core_lo, a, *v)?;
sink.end_segment(a_pos, c, Some(la.owner.y))?;
prev_loc = Some(la); } else {
self.walk(a, *v, sink, budget)?;
sink.end_segment(a_pos, c, None)?;
prev_loc = Some(if la.contains_core(c) {
la
} else {
self.locate(c)?
});
}
}
prev = Some(*v);
}
Ok(())
}
fn walk<V: Vertex, S: RouteSink<V>>(
self,
a: V,
c: V,
sink: &mut S,
budget: &mut i64,
) -> Result<(), TileError> {
let (extent, buffer) = (i64::from(self.extent), i64::from(self.buffer));
let wide = |p: Coord<i32>| Coord {
x: i64::from(p.x),
y: i64::from(p.y),
};
let (lo, hi) = if a.position().y <= c.position().y {
(wide(a.position()), wide(c.position()))
} else {
(wide(c.position()), wide(a.position()))
};
let (dx, dy) = (hi.x - lo.x, hi.y - lo.y);
for ty in (lo.y - buffer).div_euclid(extent)..=(hi.y + buffer).div_euclid(extent) {
let base = ty * extent;
let start = (base - buffer).max(lo.y) - lo.y;
let end = (base + extent - 1 + buffer).min(hi.y) - lo.y;
let x_at = |off: i64, up: bool| match off {
0 => lo.x,
_ if off == dy => hi.x,
_ if up => lo.x - mul_div_floor(-off, dx, dy),
_ => lo.x + mul_div_floor(off, dx, dy),
};
let (x_min, x_max) = if dy == 0 {
(lo.x.min(hi.x), lo.x.max(hi.x))
} else if dx >= 0 {
(x_at(start, true), x_at(end, false))
} else {
(x_at(end, true), x_at(start, false))
};
let (first, last) = (
(x_min - buffer).div_euclid(extent),
(x_max + buffer).div_euclid(extent),
);
*budget -= (last - first + 1).max(1);
if *budget < 0 {
return Err(TileError::TooManyTiles);
}
if first > last {
continue;
}
let fits = |v: i64| i32::try_from(v).map_err(|_| TileError::Overflow);
fits(first * extent - buffer)?;
fits(last * extent + extent - 1 + buffer)?;
fits(base - buffer)?;
fits(base + extent - 1 + buffer)?;
let (ty, origin_y) = (fits(ty)?, fits(base)?);
for tx in fits(first)?..=fits(last)? {
let origin = Coord {
x: tx * self.extent,
y: origin_y,
};
sink.emit(TileId::new(tx, ty), origin, a, c)?;
}
}
Ok(())
}
pub(crate) fn tile_buffered_bounds(
self,
tile: TileId,
) -> Result<(Coord<i32>, Coord<i32>), TileError> {
let base_x = tile.x.checked_mul(self.extent).ok_or(TileError::Overflow)?;
let base_y = tile.y.checked_mul(self.extent).ok_or(TileError::Overflow)?;
let reach = (self.extent - 1)
.checked_add(self.buffer)
.ok_or(TileError::Overflow)?;
Ok((
Coord {
x: base_x.checked_sub(self.buffer).ok_or(TileError::Overflow)?,
y: base_y.checked_sub(self.buffer).ok_or(TileError::Overflow)?,
},
Coord {
x: base_x.checked_add(reach).ok_or(TileError::Overflow)?,
y: base_y.checked_add(reach).ok_or(TileError::Overflow)?,
},
))
}
pub(crate) fn inner_tile(
self,
mut points: impl Iterator<Item = Coord<i32>>,
) -> Option<(TileId, Coord<i32>)> {
let first = points.next()?;
let located = self.locate(first).ok()?;
(located.contains_inner(first) && points.all(|p| located.contains_inner(p)))
.then_some((located.owner, located.core_lo))
}
fn locate(self, c: Coord<i32>) -> Result<Located, TileError> {
let owner = tile_of(c, self.extent);
let (min, max) = self.tile_buffered_bounds(owner)?;
Ok(Located {
owner,
core_lo: shift(min, self.buffer),
core_hi: shift(max, -self.buffer),
inner_lo: shift(min, 2 * self.buffer),
inner_hi: shift(max, -2 * self.buffer),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Emits(usize);
impl RouteSink<Coord<i32>> for Emits {
fn begin_polyline(&mut self) {}
fn begin_segment(&mut self) {}
fn emit(
&mut self,
_: TileId,
_: Coord<i32>,
_: Coord<i32>,
_: Coord<i32>,
) -> Result<(), TileError> {
self.0 += 1;
Ok(())
}
}
#[test]
fn mul_div_floor_is_exact_past_i64() {
let reference = |n: i64, m: i64, d: i64| {
i64::try_from((i128::from(n) * i128::from(m)).div_euclid(i128::from(d))).unwrap()
};
for (n, m, d) in [
(4_000_000_000, 4_294_967_295, 4_294_967_296),
(-3_999_999_999, 4_294_967_295, 4_000_000_000),
(12, -7, 5),
] {
assert_eq!(mul_div_floor(n, m, d), reference(n, m, d), "{n}·{m}/{d}");
}
}
#[test]
fn budget_bounds_segments_inside_one_tile() {
let grid = Grid::new(4096, 8).expect("config");
let line = [(100, 100), (200, 100), (300, 100)].map(|(x, y)| Coord { x, y });
let (mut budget, mut sink) = (1, Emits(0));
let err = grid.route_within(&line, &mut sink, &mut budget).err();
assert_eq!(err, Some(TileError::TooManyTiles));
assert_eq!(
sink.0, 1,
"the first segment was routed before the budget ran out"
);
}
struct Tiles(Vec<TileId>);
impl RouteSink<Coord<i32>> for Tiles {
fn begin_polyline(&mut self) {}
fn begin_segment(&mut self) {}
fn emit(
&mut self,
tile: TileId,
_: Coord<i32>,
_: Coord<i32>,
_: Coord<i32>,
) -> Result<(), TileError> {
self.0.push(tile);
Ok(())
}
}
fn walked(grid: Grid, a: Coord<i32>, c: Coord<i32>) -> Result<(Vec<TileId>, i64), TileError> {
let mut sink = Tiles(Vec::new());
let mut budget = MAX_TILE_VISITS;
grid.walk(a, c, &mut sink, &mut budget)?;
Ok((sink.0, MAX_TILE_VISITS - budget))
}
fn scanned(grid: Grid, a: Coord<i32>, c: Coord<i32>) -> Result<Vec<TileId>, TileError> {
let (extent, buffer) = (i64::from(grid.extent), i64::from(grid.buffer));
let tile = |v: i32, d: i64| i32::try_from((i64::from(v) + d).div_euclid(extent)).unwrap();
let mut out = Vec::new();
for ty in tile(a.y.min(c.y), -buffer)..=tile(a.y.max(c.y), buffer) {
for tx in tile(a.x.min(c.x), -buffer)..=tile(a.x.max(c.x), buffer) {
let tile = TileId::new(tx, ty);
let (min, max) = grid.tile_buffered_bounds(tile)?;
if segment_intersects(a, c, min, max) {
out.push(tile);
}
}
}
Ok(out)
}
#[test]
fn walk_matches_bounding_box_scan() {
let mut state = 0x9E37_79B9_7F4A_7C15_u64;
let mut below = |n: u32| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
i64::from(u32::try_from(state % u64::from(n)).unwrap())
};
let mut overflows = 0;
for case in 0..20_000 {
let extent: u32 = [1, 2, 3, 5, 7, 10, 25, 4096][case % 8];
let buffer = u16::try_from(below(extent.div_ceil(2))).unwrap();
let grid = Grid::new(extent, buffer).unwrap();
let span = extent * 40;
let origin = match case % 5 {
0 => i64::from(i32::MIN),
1 => i64::from(i32::MAX) - i64::from(span),
_ => -i64::from(span / 2),
};
let mut coord = || {
let mut v = || i32::try_from(origin + below(span)).unwrap();
Coord { x: v(), y: v() }
};
let (a, mut c) = (coord(), coord());
match below(4) {
0 => c.y = a.y,
1 => c.x = a.x,
2 => c.x = i32::try_from(i64::from(a.x) + below(3) - 1).unwrap_or(a.x),
_ => {}
}
let walk = walked(grid, a, c).map(|(tiles, charged)| {
let tile_y = |v: i32, d: i64| (i64::from(v) + d).div_euclid(i64::from(extent));
let rows = tile_y(a.y.max(c.y), i64::from(buffer))
- tile_y(a.y.min(c.y), -i64::from(buffer))
+ 1;
let mut visited_rows: Vec<i32> = tiles.iter().map(|t| t.y).collect();
visited_rows.dedup();
let gap_rows = rows - i64::try_from(visited_rows.len()).unwrap();
assert!(
buffer == 0 || gap_rows == 0,
"only unbuffered tiles leave gaps"
);
assert_eq!(
charged,
i64::try_from(tiles.len()).unwrap() + gap_rows,
"one charge per visit or gap row"
);
tiles
});
overflows += usize::from(walk == Err(TileError::Overflow));
assert_eq!(walk, scanned(grid, a, c), "{extent}/{buffer} {a:?}-{c:?}");
}
assert!(
overflows > 100,
"the i32 limits are exercised ({overflows})"
);
}
}