#![expect(
clippy::cast_sign_loss,
reason = "level (i32) cast to u8 after clamping to valid range"
)]
#![expect(
clippy::cast_possible_truncation,
reason = "level (i32->u8) after clamping"
)]
use crate::s1;
use crate::s1::ChordAngle;
use crate::s2::closest_edge_query::{CellTarget, ClosestEdgeQuery, PointTarget};
use crate::s2::coords::Level;
use crate::s2::metric::MIN_WIDTH;
use crate::s2::shape_index::ShapeIndex;
use crate::s2::shape_index_region::ShapeIndexRegion;
use crate::s2::{Cap, Cell, CellId, Point, Rect, Region};
#[derive(Debug)]
pub struct ShapeIndexBufferedRegion<'a> {
index: &'a ShapeIndex,
radius: ChordAngle,
radius_successor: ChordAngle,
}
impl<'a> ShapeIndexBufferedRegion<'a> {
pub fn new(index: &'a ShapeIndex, radius: ChordAngle) -> Self {
ShapeIndexBufferedRegion {
index,
radius,
radius_successor: radius.successor(),
}
}
pub fn from_angle(index: &'a ShapeIndex, radius: s1::Angle) -> Self {
Self::new(index, ChordAngle::from_angle(radius))
}
pub fn index(&self) -> &'a ShapeIndex {
self.index
}
pub fn radius(&self) -> ChordAngle {
self.radius
}
}
impl Region for ShapeIndexBufferedRegion<'_> {
fn cap_bound(&self) -> Cap {
let orig = ShapeIndexRegion::new(self.index).cap_bound();
if orig.is_empty() {
return Cap::empty();
}
let combined = if orig.chord_radius().is_special() || self.radius.is_special() {
ChordAngle::STRAIGHT
} else {
orig.chord_radius() + self.radius
};
Cap::from_center_chord_angle(orig.center(), combined)
}
fn rect_bound(&self) -> Rect {
let orig = ShapeIndexRegion::new(self.index).rect_bound();
orig.expanded_by_distance(self.radius.to_angle())
}
fn cell_union_bound(&self) -> Vec<CellId> {
let radians = self.radius.to_angle().radians();
let max_level = i32::from(MIN_WIDTH.min_level(radians)) - 1;
if max_level < 0 {
return Cap::full().cell_union_bound();
}
let orig_cells = ShapeIndexRegion::new(self.index).cell_union_bound();
let mut result = Vec::new();
for id in orig_cells {
if id.is_face() {
return Cap::full().cell_union_bound();
}
let level = std::cmp::min(Level::new(max_level as u8), id.level() - 1u8);
let neighbors = id.vertex_neighbors(level);
result.extend(neighbors);
}
result
}
fn contains_cell(&self, cell: &Cell) -> bool {
if self.radius_successor > ChordAngle::STRAIGHT {
return true;
}
if ShapeIndexRegion::new(self.index).contains_cell(cell) {
return true;
}
let cap = cell.cap_bound();
if self.radius < cap.chord_radius() {
return false;
}
let query = ClosestEdgeQuery::new(self.index);
let target = PointTarget::new(cell.center());
query.is_distance_less(&target, self.radius_successor - cap.chord_radius())
}
fn intersects_cell(&self, cell: &Cell) -> bool {
let query = ClosestEdgeQuery::new(self.index);
let target = CellTarget::new(*cell);
query.is_distance_less(&target, self.radius_successor)
}
fn contains_point(&self, p: &Point) -> bool {
let query = ClosestEdgeQuery::new(self.index);
let target = PointTarget::new(*p);
query.is_distance_less(&target, self.radius_successor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::s1::Angle;
use crate::s2::region_coverer::RegionCoverer;
use crate::s2::text_format;
fn make_index_with_points(points: &[Point]) -> ShapeIndex {
use crate::s2::point_vector::PointVector;
let mut index = ShapeIndex::new();
let pv = PointVector::new(points.to_vec());
index.add(Box::new(pv));
index.build();
index
}
fn make_index_with_polyline(text: &str) -> ShapeIndex {
use crate::s2::lax_polyline::LaxPolyline;
let pl = text_format::make_polyline(text);
let vertices: Vec<Point> = (0..pl.num_vertices()).map(|i| pl.vertex(i)).collect();
let mut index = ShapeIndex::new();
let lax = LaxPolyline::new(vertices);
index.add(Box::new(lax));
index.build();
index
}
#[test]
fn test_empty_index() {
let index = ShapeIndex::new();
let region =
ShapeIndexBufferedRegion::new(&index, ChordAngle::from_angle(Angle::from_degrees(1.0)));
let cells = region.cell_union_bound();
assert!(cells.is_empty());
}
#[test]
fn test_point_zero_radius() {
let p = Point::from_coords(1.0, 0.0, 0.0);
let index = make_index_with_points(&[p]);
let region = ShapeIndexBufferedRegion::new(&index, ChordAngle::ZERO);
assert!(region.contains_point(&p));
let far = Point::from_coords(0.0, 1.0, 0.0);
assert!(!region.contains_point(&far));
}
#[test]
fn test_point_with_radius() {
let p = Point::from_coords(1.0, 0.0, 0.0);
let index = make_index_with_points(&[p]);
let radius = ChordAngle::from_angle(Angle::from_degrees(5.0));
let region = ShapeIndexBufferedRegion::new(&index, radius);
let nearby = Point::from_coords(1.0, 0.01, 0.0).normalize();
assert!(region.contains_point(&nearby));
let far = Point::from_coords(0.0, 1.0, 0.0);
assert!(!region.contains_point(&far));
}
#[test]
fn test_point_set() {
let p1 = text_format::parse_point("0:0");
let p2 = text_format::parse_point("0:90");
let p3 = text_format::parse_point("90:0");
let index = make_index_with_points(&[p1, p2, p3]);
let radius = ChordAngle::from_angle(Angle::from_degrees(5.0));
let region = ShapeIndexBufferedRegion::new(&index, radius);
assert!(region.contains_point(&text_format::parse_point("1:0")));
assert!(region.contains_point(&text_format::parse_point("0:91")));
assert!(region.contains_point(&text_format::parse_point("89:0")));
assert!(!region.contains_point(&text_format::parse_point("45:45")));
}
#[test]
fn test_polyline_buffered() {
let index = make_index_with_polyline("0:0, 0:10, 10:10");
let radius = ChordAngle::from_angle(Angle::from_degrees(2.0));
let region = ShapeIndexBufferedRegion::new(&index, radius);
let on_line = Point::from_coords(1.0, 0.0, 0.0);
assert!(region.contains_point(&on_line));
let nearby = text_format::parse_point("1:5");
assert!(region.contains_point(&nearby));
let far = text_format::parse_point("45:45");
assert!(!region.contains_point(&far));
}
#[test]
fn test_huge_buffer_radius() {
let p = Point::from_coords(1.0, 0.0, 0.0);
let index = make_index_with_points(&[p]);
let radius = ChordAngle::from_angle(Angle::from_degrees(200.0));
let region = ShapeIndexBufferedRegion::new(&index, radius);
let cells = region.cell_union_bound();
assert_eq!(cells.len(), 6);
}
#[test]
fn test_cap_bound_not_empty() {
let p = Point::from_coords(1.0, 0.0, 0.0);
let index = make_index_with_points(&[p]);
let radius = ChordAngle::from_angle(Angle::from_degrees(10.0));
let region = ShapeIndexBufferedRegion::new(&index, radius);
let cap = region.cap_bound();
assert!(!cap.is_empty());
assert!(cap.contains_point(p));
}
#[test]
fn test_rect_bound_expansion() {
let index = make_index_with_polyline("0:0, 0:10");
let radius = ChordAngle::from_angle(Angle::from_degrees(1.0));
let region = ShapeIndexBufferedRegion::new(&index, radius);
let rect = region.rect_bound();
let orig_rect = ShapeIndexRegion::new(region.index()).rect_bound();
assert!(rect.lat.length() > orig_rect.lat.length());
}
#[test]
fn test_full_polygon() {
let index = text_format::make_index("# # full");
let radius = ChordAngle::from_angle(Angle::from_degrees(2.0));
let region = ShapeIndexBufferedRegion::new(&index, radius);
let covering = RegionCoverer::new().covering(®ion);
assert_eq!(6, covering.num_cells());
for &id in covering.cell_ids() {
assert!(id.is_face(), "expected face cell, got {id:?}");
}
}
#[test]
fn test_full_after_buffering() {
let index = text_format::make_index("0:0 | 0:90 | 0:180 | 0:-90 | 90:0 | -90:0 # #");
let radius = ChordAngle::from_angle(Angle::from_degrees(60.0));
let region = ShapeIndexBufferedRegion::new(&index, radius);
let coverer = RegionCoverer::new().max_cells(1000);
let covering = coverer.covering(®ion);
assert_eq!(6, covering.num_cells());
for &id in covering.cell_ids() {
assert!(id.is_face());
}
}
}