use super::neighbors;
use crate::{CellIndex, LatLng, Resolution, TWO_PI, error::InvalidGeometry};
use ahash::{HashSet, HashSetExt};
use either::Either;
use float_eq::float_eq;
use geo::{
BooleanOps as _, BoundingRect as _, Centroid as _, Coord, CoordsIter as _,
Intersects, Line, LineString, MultiPolygon, Polygon, Rect, Relate as _,
ToRadians as _,
algorithm::{
coordinate_position::{CoordPos, coord_pos_relative_to_ring},
relate::PreparedGeometry,
},
coord,
};
use std::{
cmp,
f64::consts::{FRAC_PI_2, PI},
};
#[derive(Debug, Clone)]
pub struct Tiler {
resolution: Resolution,
containment_mode: ContainmentMode,
convert_to_rads: bool,
transmeridian_heuristic_enabled: bool,
geom: MultiPolygon,
}
pub struct AnnotatedCell {
pub cell: CellIndex,
pub is_fully_contained: bool,
}
impl Tiler {
pub fn add(&mut self, mut polygon: Polygon) -> Result<(), InvalidGeometry> {
if self.convert_to_rads {
polygon.to_radians_in_place();
}
ring_is_valid(polygon.exterior())?;
for interior in polygon.interiors() {
ring_is_valid(interior)?;
}
if self.transmeridian_heuristic_enabled && is_transmeridian(&polygon) {
for fixed_polygon in fix_transmeridian(polygon).0 {
self.geom.0.push(fixed_polygon);
}
} else {
self.geom.0.push(polygon);
}
Ok(())
}
pub fn add_batch(
&mut self,
geoms: impl IntoIterator<Item = Polygon>,
) -> Result<(), InvalidGeometry> {
for polygon in geoms {
self.add(polygon)?;
}
Ok(())
}
#[must_use]
pub fn coverage_size_hint(&self) -> usize {
const POLYGON_TO_CELLS_BUFFER: usize = 12;
self.geom
.iter()
.map(|polygon| {
let estimated_count = bbox_hex_estimate(
&polygon.bounding_rect().expect("valid polygon"),
self.resolution,
);
let vertex_count = polygon
.interiors()
.iter()
.chain(std::iter::once(polygon.exterior()))
.map(|line| line.coords_count() - 1)
.sum();
cmp::max(estimated_count, vertex_count)
+ POLYGON_TO_CELLS_BUFFER
})
.sum()
}
pub fn into_coverage(self) -> impl Iterator<Item = CellIndex> {
self.into_annotated_coverage().map(|value| value.cell)
}
pub fn into_annotated_coverage(
self,
) -> impl Iterator<Item = AnnotatedCell> {
let predicate =
ContainmentPredicate::new(&self.geom, self.containment_mode);
let mut seen = HashSet::new();
let mut scratchpad = [0; 7];
let mut outlines = self.hex_outline(
self.resolution,
&mut seen,
&mut scratchpad,
&predicate,
);
if outlines.is_empty()
&& self.containment_mode == ContainmentMode::Covers
{
let centroid = self.geom.centroid().expect("centroid");
return Either::Left(std::iter::once(AnnotatedCell {
cell: LatLng::from_radians(centroid.y(), centroid.x())
.expect("valid coordinate")
.to_cell(self.resolution),
is_fully_contained: false,
}));
}
let mut candidates = outermost_inner_cells(
&outlines,
&mut seen,
&mut scratchpad,
&predicate,
);
let mut next_gen = Vec::with_capacity(candidates.len() * 7);
let mut new_seen = HashSet::with_capacity(seen.len());
if self.containment_mode == ContainmentMode::ContainsBoundary {
outlines.retain(|&(_, is_fully_contained)| is_fully_contained);
candidates.retain(|&(_, is_fully_contained)| is_fully_contained);
}
let inward_propagation = std::iter::from_fn(move || {
if candidates.is_empty() {
return None;
}
for &(cell, _) in &candidates {
debug_assert!(
self.geom.relate(&cell_boundary(cell)).is_covers(),
"cell index {cell} in polygon"
);
let count = neighbors(cell, &mut scratchpad);
next_gen.extend(scratchpad[0..count].iter().filter_map(
|candidate| {
let index = CellIndex::new_unchecked(*candidate);
new_seen.insert(index);
seen.insert(index).then_some((index, true))
},
));
}
let curr_gen = candidates.clone();
std::mem::swap(&mut next_gen, &mut candidates);
next_gen.clear();
std::mem::swap(&mut new_seen, &mut seen);
new_seen.clear();
Some(curr_gen.into_iter())
});
Either::Right(
outlines
.into_iter()
.chain(inward_propagation.flatten())
.map(|(cell, is_fully_contained)| AnnotatedCell {
cell,
is_fully_contained,
}),
)
}
fn hex_outline(
&self,
resolution: Resolution,
already_seen: &mut HashSet<CellIndex>,
scratchpad: &mut [u64],
predicate: &ContainmentPredicate<'_>,
) -> Vec<(CellIndex, bool)> {
let outlines = self
.interiors()
.chain(self.exteriors())
.flat_map(|ring| get_edge_cells(ring, resolution))
.filter(|cell| already_seen.insert(*cell))
.collect::<Vec<_>>();
already_seen.clear();
outlines.into_iter().fold(Vec::new(), |mut acc, cell| {
let count = neighbors(cell, scratchpad);
acc.extend(scratchpad[0..count].iter().filter_map(|candidate| {
let index = CellIndex::new_unchecked(*candidate);
already_seen
.insert(index)
.then_some(index)
.and_then(|index| {
let result = predicate.apply(index);
result
.is_a_match
.then_some((index, result.is_fully_contained))
})
}));
acc
})
}
fn exteriors(&self) -> impl Iterator<Item = &LineString> {
self.geom.0.iter().map(Polygon::exterior)
}
fn interiors(&self) -> impl Iterator<Item = &LineString> {
self.geom
.0
.iter()
.flat_map(|polygon| polygon.interiors().iter())
}
}
pub struct TilerBuilder {
resolution: Resolution,
containment_mode: ContainmentMode,
convert_to_rads: bool,
transmeridian_heuristic_enabled: bool,
}
impl TilerBuilder {
#[must_use]
pub const fn new(resolution: Resolution) -> Self {
Self {
resolution,
containment_mode: ContainmentMode::ContainsCentroid,
convert_to_rads: true,
transmeridian_heuristic_enabled: true,
}
}
#[must_use]
pub const fn disable_radians_conversion(mut self) -> Self {
self.convert_to_rads = false;
self
}
#[must_use]
pub const fn containment_mode(mut self, mode: ContainmentMode) -> Self {
self.containment_mode = mode;
self
}
#[must_use]
pub const fn disable_transmeridian_heuristic(mut self) -> Self {
self.transmeridian_heuristic_enabled = false;
self
}
#[must_use]
pub fn build(self) -> Tiler {
Tiler {
resolution: self.resolution,
containment_mode: self.containment_mode,
convert_to_rads: self.convert_to_rads,
transmeridian_heuristic_enabled: self
.transmeridian_heuristic_enabled,
geom: MultiPolygon::new(Vec::new()),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ContainmentMode {
ContainsCentroid,
ContainsBoundary,
IntersectsBoundary,
Covers,
}
struct PredicateResult {
is_a_match: bool,
is_fully_contained: bool,
}
#[expect(clippy::large_enum_variant, reason = "used once, on the stack")]
enum ContainmentPredicate<'geom> {
ContainsCentroid(&'geom MultiPolygon, MultiBBoxes),
IntersectsBoundary(PreparedGeometry<'geom, &'geom MultiPolygon>),
}
impl<'geom> ContainmentPredicate<'geom> {
fn new(
geom: &'geom MultiPolygon,
containment_mode: ContainmentMode,
) -> Self {
match containment_mode {
ContainmentMode::ContainsCentroid => {
let bboxes = MultiBBoxes(
geom.iter()
.map(|polygon| BBoxes {
exterior: polygon
.exterior()
.bounding_rect()
.expect("exterior bbox"),
interiors: polygon
.interiors()
.iter()
.map(|ring| {
ring.bounding_rect().expect("interior bbox")
})
.collect(),
})
.collect(),
);
Self::ContainsCentroid(geom, bboxes)
}
ContainmentMode::ContainsBoundary
| ContainmentMode::IntersectsBoundary
| ContainmentMode::Covers => {
let prepared_geom = PreparedGeometry::from(geom);
Self::IntersectsBoundary(prepared_geom)
}
}
}
fn apply(&self, cell: CellIndex) -> PredicateResult {
match self {
Self::ContainsCentroid(geom, bboxes) => {
let ll = LatLng::from(cell);
let coord = coord! { x: ll.lng_radians(), y: ll.lat_radians() };
let is_a_match =
geom.iter().enumerate().any(|(poly_idx, polygon)| {
ring_contains_centroid(
polygon.exterior(),
&bboxes.0[poly_idx].exterior,
coord,
) && !polygon.interiors().iter().enumerate().any(
|(ring_idx, ring)| {
ring_contains_centroid(
ring,
&bboxes.0[poly_idx].interiors[ring_idx],
coord,
)
},
)
});
PredicateResult {
is_a_match,
is_fully_contained: true,
}
}
Self::IntersectsBoundary(geom) => {
let boundary = cell_boundary(cell);
let relation = geom.relate(&boundary);
PredicateResult {
is_a_match: relation.is_intersects(),
is_fully_contained: relation.is_covers(),
}
}
}
}
}
fn outermost_inner_cells(
outlines: &[(CellIndex, bool)],
already_seen: &mut HashSet<CellIndex>,
scratchpad: &mut [u64],
predicate: &ContainmentPredicate<'_>,
) -> Vec<(CellIndex, bool)> {
outlines.iter().fold(Vec::new(), |mut acc, &(cell, _)| {
let count = neighbors(cell, scratchpad);
acc.extend(scratchpad[0..count].iter().filter_map(|candidate| {
let index = CellIndex::new_unchecked(*candidate);
already_seen
.insert(index)
.then_some(index)
.and_then(|index| {
let result = predicate.apply(index);
result
.is_a_match
.then_some((index, result.is_fully_contained))
})
}));
acc
})
}
fn get_edge_cells(
ring: &LineString,
resolution: Resolution,
) -> impl Iterator<Item = CellIndex> + '_ {
ring.lines().flat_map(move |line @ Line { start, end }| {
let count = line_hex_estimate(&line, resolution);
assert!(count <= 1 << f64::MANTISSA_DIGITS);
#[expect(
clippy::cast_precision_loss,
reason = "cannot happen thanks to assert above"
)]
(0..count).map(move |i| {
let i = i as f64;
let count = count as f64;
let lat = (start.y * (count - i) / count) + (end.y * i / count);
let lng = (start.x * (count - i) / count) + (end.x * i / count);
LatLng::from_radians(lat, lng)
.expect("finite line coordinate")
.to_cell(resolution)
})
})
}
fn line_hex_estimate(line: &Line, resolution: Resolution) -> u64 {
const PENT_DIAMETER_RADS: [f64; 16] = [
0.32549355508382627,
0.11062000431697926,
0.0431531246375496,
0.015280278825461551,
0.006095981694441515,
0.00217237586248339,
0.0008694532999397082,
0.0003101251537809772,
0.00012417902430910614,
0.00004429922220615181,
0.00001773927716796858,
0.000006328371112691009,
0.0000025341705472716865,
0.0000009040511973807097,
0.00000036202412300873475,
0.00000012915013523209886,
];
let pentagon_diameter = PENT_DIAMETER_RADS[usize::from(resolution)];
let origin = LatLng::from_radians(line.start.y, line.start.x)
.expect("finite line-start coordinate");
let destination = LatLng::from_radians(line.end.y, line.end.x)
.expect("finite line-end coordinate");
let distance = origin.distance_rads(destination);
let dist_ceil = (distance / pentagon_diameter).ceil();
assert!(dist_ceil.is_finite());
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "truncate on purpose"
)]
let estimate = dist_ceil as u64;
cmp::max(estimate, 1)
}
pub fn bbox_hex_estimate(bbox: &Rect, resolution: Resolution) -> usize {
const PENT_AREA_RADS2: [f64; 16] = [
0.05505118472518226,
0.006358420186890303,
0.0009676234334810151,
0.00012132336301389888,
0.000019309418286620768,
0.0000024521770265310696,
0.0000003928026439666205,
0.00000004997535264470275,
0.000000008012690511075445,
0.0000000010197039091132572,
0.00000000016351353999538285,
0.000000000020809697203105007,
0.000000000003336979666606075,
0.0000000000004246859893033221,
0.00000000000006810153522091642,
0.000000000000008667056198238203,
];
let pentagon_area_rads2 = PENT_AREA_RADS2[usize::from(resolution)];
let min = bbox.min();
let max = bbox.max();
let p1 =
LatLng::from_radians(min.y, min.x).expect("finite bbox-min coordinate");
let p2 =
LatLng::from_radians(max.y, max.x).expect("finite bbox-max coordinate");
let diagonal = p1.distance_rads(p2);
let d1 = (p1.lng_radians() - p2.lng_radians()).abs();
let d2 = (p1.lat_radians() - p2.lat_radians()).abs();
let (width, length) = if d1 < d2 { (d1, d2) } else { (d2, d1) };
#[expect(clippy::suspicious_operation_groupings, reason = "false positive")]
let area = (diagonal * diagonal) / (length / width);
let estimate = (area / pentagon_area_rads2).ceil();
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "truncate on purpose"
)]
let estimate = estimate as usize;
cmp::max(estimate, 1)
}
fn is_transmeridian(geom: &Polygon) -> bool {
geom.exterior()
.lines()
.any(|line| (line.start.x - line.end.x).abs() > PI)
}
fn fix_transmeridian(mut polygon: Polygon) -> MultiPolygon {
let west = Rect::new(
coord! { x: PI, y: -FRAC_PI_2},
coord! { x: TWO_PI, y: FRAC_PI_2},
)
.to_polygon();
let east = Rect::new(
coord! { x: 0., y: -FRAC_PI_2},
coord! { x: PI, y: FRAC_PI_2},
)
.to_polygon();
shift_transmeridian(&mut polygon);
let mut fixed = polygon.intersection(&west);
unshift_transmeridian(&mut fixed);
fix_clipping_boundary(&mut fixed, true);
let mut other = polygon.intersection(&east);
fix_clipping_boundary(&mut other, false);
fixed.0.extend(other.0);
fixed
}
fn shift_transmeridian(geom: &mut Polygon) {
geom.exterior_mut(shift_transmeridian_ring);
geom.interiors_mut(|interiors| {
for interior in interiors {
shift_transmeridian_ring(interior);
}
});
}
fn unshift_transmeridian(geom: &mut MultiPolygon) {
for polygon in geom.iter_mut() {
polygon.exterior_mut(unshift_transmeridian_ring);
polygon.interiors_mut(|interiors| {
for interior in interiors {
unshift_transmeridian_ring(interior);
}
});
}
}
fn fix_clipping_boundary(geom: &mut MultiPolygon, is_west: bool) {
for polygon in geom.iter_mut() {
polygon.exterior_mut(|exterior| {
fix_ring_clipping_boundary(exterior, is_west);
});
polygon.interiors_mut(|interiors| {
for interior in interiors {
fix_ring_clipping_boundary(interior, is_west);
}
});
}
}
pub fn ring_is_valid(ring: &LineString) -> Result<(), InvalidGeometry> {
if ring.0.len() < 4 {
return Err(InvalidGeometry::new(
"invalid ring (not enough coordinate)",
));
}
if !ring.coords().all(|coord| super::coord_is_valid(*coord)) {
return Err(InvalidGeometry::new(
"every coordinate of the exterior must be valid",
));
}
Ok(())
}
fn shift_transmeridian_ring(ring: &mut LineString) {
for coord in ring.coords_mut() {
coord.x += f64::from(coord.x < 0.) * TWO_PI;
}
}
fn unshift_transmeridian_ring(ring: &mut LineString) {
for coord in ring.coords_mut() {
coord.x -= f64::from(coord.x >= PI) * TWO_PI;
}
}
fn fix_ring_clipping_boundary(ring: &mut LineString, is_west: bool) {
const ROUNDING_EPSILON: f64 = 1e-6;
let (bad_value, fixed_value) = if is_west {
let mut bad_value = PI;
for coord in ring.coords() {
if float_eq!(coord.x, PI, abs <= ROUNDING_EPSILON) {
bad_value = coord.x;
break;
}
bad_value = bad_value.min(coord.x);
}
(bad_value, -PI)
} else {
let mut bad_value = -PI;
for coord in ring.coords() {
if float_eq!(coord.x, -PI, abs <= ROUNDING_EPSILON) {
bad_value = coord.x;
break;
}
bad_value = bad_value.max(coord.x);
}
(bad_value, PI)
};
#[expect(clippy::float_cmp, reason = "we want exact equality")]
for coord in ring.coords_mut() {
if coord.x == bad_value {
coord.x = fixed_value;
}
}
}
struct MultiBBoxes(Vec<BBoxes>);
struct BBoxes {
exterior: Rect,
interiors: Vec<Rect>,
}
fn ring_contains_centroid(
ring: &LineString,
bbox: &Rect,
mut coord: Coord,
) -> bool {
if !bbox.intersects(&coord) {
return false;
}
match coord_pos_relative_to_ring(coord, ring) {
CoordPos::Inside => true,
CoordPos::Outside => false,
CoordPos::OnBoundary => {
coord.y += f64::EPSILON;
coord_pos_relative_to_ring(coord, ring) == CoordPos::Inside
}
}
}
pub fn cell_boundary(cell: CellIndex) -> MultiPolygon {
let boundary = LineString(
cell.boundary()
.iter()
.copied()
.map(|ll| {
coord! {
x: ll.lng_radians(),
y: ll.lat_radians()
}
})
.collect(),
);
let polygon = Polygon::new(boundary, Vec::new());
if is_transmeridian(&polygon) {
fix_transmeridian(polygon)
} else {
MultiPolygon::new(vec![polygon])
}
}