#![expect(
clippy::cast_possible_truncation,
reason = "level (i32) -> u8 after clamping to valid cell level range"
)]
use crate::s1::Angle;
use crate::s2::coords::Level;
use crate::s2::metric;
use crate::s2::predicates::DBL_EPSILON;
use crate::s2::{CellId, LatLng, Point};
pub const MAX_SNAP_RADIUS: Angle = Angle::from_radians(70.0 * std::f64::consts::PI / 180.0);
pub trait SnapFunction: Send + std::fmt::Debug {
fn snap_radius(&self) -> Angle;
fn snap_point(&self, point: Point) -> Point;
fn min_vertex_separation(&self) -> Angle;
fn min_edge_vertex_separation(&self) -> Angle;
fn clone_snap(&self) -> Box<dyn SnapFunction>;
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IdentitySnapFunction {
snap_radius: Angle,
}
impl IdentitySnapFunction {
pub fn new(snap_radius: Angle) -> Self {
debug_assert!(snap_radius <= MAX_SNAP_RADIUS);
IdentitySnapFunction { snap_radius }
}
}
impl SnapFunction for IdentitySnapFunction {
fn snap_radius(&self) -> Angle {
self.snap_radius
}
fn snap_point(&self, point: Point) -> Point {
point
}
fn min_vertex_separation(&self) -> Angle {
self.snap_radius
}
fn min_edge_vertex_separation(&self) -> Angle {
Angle::from_radians(0.5 * self.snap_radius.radians())
}
fn clone_snap(&self) -> Box<dyn SnapFunction> {
Box::new(self.clone())
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct S2CellIdSnapFunction {
level: Level,
snap_radius: Angle,
}
impl S2CellIdSnapFunction {
pub fn new(level: impl Into<Level>) -> Self {
let level = level.into();
let snap_radius = Self::min_snap_radius_for_level(level);
S2CellIdSnapFunction { level, snap_radius }
}
pub fn with_snap_radius(level: impl Into<Level>, snap_radius: Angle) -> Self {
let level = level.into();
let min = Self::min_snap_radius_for_level(level);
let snap_radius = if snap_radius.radians() > min.radians() {
snap_radius
} else {
min
};
debug_assert!(snap_radius >= min);
debug_assert!(snap_radius <= MAX_SNAP_RADIUS);
S2CellIdSnapFunction { level, snap_radius }
}
pub fn level(&self) -> Level {
self.level
}
pub fn min_snap_radius_for_level(level: impl Into<Level>) -> Angle {
let level = level.into();
let max_diag_value = metric::MAX_DIAG.value(level);
Angle::from_radians(0.5 * max_diag_value + 4.0 * DBL_EPSILON)
}
pub fn level_for_max_snap_radius(snap_radius: Angle) -> Level {
metric::MAX_DIAG.min_level(2.0 * (snap_radius.radians() - 4.0 * DBL_EPSILON))
}
}
impl SnapFunction for S2CellIdSnapFunction {
fn snap_radius(&self) -> Angle {
self.snap_radius
}
fn snap_point(&self, point: Point) -> Point {
CellId::from_point(&point)
.parent_at_level(self.level)
.to_point()
}
fn min_vertex_separation(&self) -> Angle {
let snap_radius = self.snap_radius.radians();
let min_edge = metric::MIN_EDGE.value(self.level);
let max_diag = metric::MAX_DIAG.value(self.level);
let constant_bound = min_edge;
let proportional_bound = 0.548 * snap_radius;
let asymptotic_bound = snap_radius - 0.5 * max_diag;
Angle::from_radians(constant_bound.max(proportional_bound).max(asymptotic_bound))
}
fn min_edge_vertex_separation(&self) -> Angle {
let snap_radius = self.snap_radius.radians();
let min_diag = metric::MIN_DIAG.value(self.level);
let min_sr = Self::min_snap_radius_for_level(self.level).radians();
if (snap_radius - min_sr).abs() < 1e-15 {
return Angle::from_radians(0.565 * min_diag);
}
let vertex_sep = self.min_vertex_separation().radians();
let constant_bound = 0.397 * min_diag;
let proportional_bound = 0.219 * snap_radius;
let asymptotic_bound = if snap_radius > 0.0 {
0.5 * (vertex_sep / snap_radius) * vertex_sep
} else {
0.0
};
Angle::from_radians(constant_bound.max(proportional_bound).max(asymptotic_bound))
}
fn clone_snap(&self) -> Box<dyn SnapFunction> {
Box::new(self.clone())
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IntLatLngSnapFunction {
exponent: i32,
snap_radius: Angle,
from_degrees: f64,
to_degrees: f64,
}
impl IntLatLngSnapFunction {
pub const MIN_EXPONENT: i32 = 0;
pub const MAX_EXPONENT: i32 = 10;
pub fn new(exponent: i32) -> Self {
debug_assert!(exponent >= Self::MIN_EXPONENT);
debug_assert!(exponent <= Self::MAX_EXPONENT);
let exponent = exponent.clamp(Self::MIN_EXPONENT, Self::MAX_EXPONENT);
let power = 10_f64.powi(exponent);
let snap_radius = Self::min_snap_radius_for_exponent(exponent);
IntLatLngSnapFunction {
exponent,
snap_radius,
from_degrees: power,
to_degrees: 1.0 / power,
}
}
pub fn with_snap_radius(exponent: i32, snap_radius: Angle) -> Self {
debug_assert!(exponent >= Self::MIN_EXPONENT);
debug_assert!(exponent <= Self::MAX_EXPONENT);
let exponent = exponent.clamp(Self::MIN_EXPONENT, Self::MAX_EXPONENT);
let power = 10_f64.powi(exponent);
let min = Self::min_snap_radius_for_exponent(exponent);
let snap_radius = if snap_radius.radians() > min.radians() {
snap_radius
} else {
min
};
IntLatLngSnapFunction {
exponent,
snap_radius,
from_degrees: power,
to_degrees: 1.0 / power,
}
}
pub fn exponent(&self) -> i32 {
self.exponent
}
pub fn min_snap_radius_for_exponent(exponent: i32) -> Angle {
let power = 10_f64.powi(exponent);
let rounding_error = std::f64::consts::FRAC_1_SQRT_2 / power;
let numerical_error = (9.0 * std::f64::consts::SQRT_2 + 1.5) * DBL_EPSILON;
Angle::from_degrees(rounding_error) + Angle::from_radians(numerical_error)
}
pub fn exponent_for_max_snap_radius(snap_radius: Angle) -> i32 {
let adjusted = snap_radius.radians() - (9.0 * std::f64::consts::SQRT_2 + 1.5) * DBL_EPSILON;
let adjusted = adjusted.max(1e-30);
let exponent = f64::log10(std::f64::consts::FRAC_1_SQRT_2 / adjusted.to_degrees());
let result = (exponent - 2.0 * DBL_EPSILON).ceil() as i32;
result.clamp(Self::MIN_EXPONENT, Self::MAX_EXPONENT)
}
}
impl SnapFunction for IntLatLngSnapFunction {
fn snap_radius(&self) -> Angle {
self.snap_radius
}
fn snap_point(&self, point: Point) -> Point {
debug_assert!(self.exponent >= 0); let ll = LatLng::from_point(point);
let lat_deg = ll.lat.degrees();
let lng_deg = ll.lng.degrees();
let lat_rounded = (lat_deg * self.from_degrees).round() * self.to_degrees;
let lng_rounded = (lng_deg * self.from_degrees).round() * self.to_degrees;
LatLng::from_degrees(lat_rounded, lng_rounded).to_point()
}
fn min_vertex_separation(&self) -> Angle {
let snap_radius = self.snap_radius.radians();
let max_movement =
Angle::from_degrees(std::f64::consts::FRAC_1_SQRT_2 * self.to_degrees).radians();
let proportional_bound: f64 = 0.471 * snap_radius;
let asymptotic_bound: f64 = snap_radius - max_movement;
Angle::from_radians(proportional_bound.max(asymptotic_bound))
}
fn min_edge_vertex_separation(&self) -> Angle {
let snap_radius = self.snap_radius.radians();
let to_rad = Angle::from_degrees(self.to_degrees).radians();
let vertex_sep = self.min_vertex_separation().radians();
let constant_bound: f64 = 0.277 * to_rad;
let proportional_bound: f64 = 0.222 * snap_radius;
let asymptotic_bound = if snap_radius > 0.0 {
0.5 * (vertex_sep / snap_radius) * vertex_sep
} else {
0.0
};
Angle::from_radians(constant_bound.max(proportional_bound).max(asymptotic_bound))
}
fn clone_snap(&self) -> Box<dyn SnapFunction> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::s2::coords::MAX_CELL_LEVEL;
use quickcheck_macros::quickcheck;
#[test]
fn test_identity_snap_zero_radius() {
let snap = IdentitySnapFunction::new(Angle::default());
assert_eq!(snap.snap_radius().radians(), 0.0);
assert_eq!(snap.min_vertex_separation().radians(), 0.0);
assert_eq!(snap.min_edge_vertex_separation().radians(), 0.0);
let p = Point::from_coords(1.0, 0.0, 0.0);
assert_eq!(snap.snap_point(p), p);
}
#[test]
fn test_identity_snap_nonzero_radius() {
let snap = IdentitySnapFunction::new(Angle::from_degrees(1.0));
assert!((snap.snap_radius().degrees() - 1.0).abs() < 1e-10);
assert!((snap.min_vertex_separation().degrees() - 1.0).abs() < 1e-10);
assert!((snap.min_edge_vertex_separation().degrees() - 0.5).abs() < 1e-10);
}
#[test]
fn test_cell_id_snap_level_30() {
let snap = S2CellIdSnapFunction::new(MAX_CELL_LEVEL);
let p = Point::from_coords(1.0, 2.0, 3.0);
let snapped = snap.snap_point(p);
let cell_id = CellId::from_point(&snapped);
assert!(cell_id.is_leaf());
}
#[test]
fn test_cell_id_snap_level_10() {
let snap = S2CellIdSnapFunction::new(10);
let p = Point::from_coords(1.0, 0.0, 0.0);
let snapped = snap.snap_point(p);
let cell_id = CellId::from_point(&snapped);
assert!(cell_id.level() >= 10);
let parent = CellId::from_point(&p).parent_at_level(10);
assert_eq!(snapped, parent.to_point());
}
#[test]
fn test_cell_id_snap_radius_properties() {
for level in [0u8, 5, 10, 15, 20, 25, 30] {
let snap = S2CellIdSnapFunction::new(level);
let sr = snap.snap_radius();
let vs = snap.min_vertex_separation();
let evs = snap.min_edge_vertex_separation();
assert!(
sr.radians() >= S2CellIdSnapFunction::min_snap_radius_for_level(level).radians()
);
assert!(vs.radians() > 0.0, "level {level}: vs={}", vs.radians());
assert!(evs.radians() > 0.0, "level {level}: evs={}", evs.radians());
assert!(vs.radians() <= 2.0 * sr.radians() + 1e-15);
assert!(evs.radians() <= vs.radians() + 1e-15);
}
}
#[test]
fn test_cell_id_level_for_max_snap_radius() {
for level in 0..=MAX_CELL_LEVEL {
let radius = S2CellIdSnapFunction::min_snap_radius_for_level(level);
assert_eq!(
S2CellIdSnapFunction::level_for_max_snap_radius(radius),
level,
"roundtrip failed for level {level}"
);
let expected = (level + 1).min(MAX_CELL_LEVEL);
assert_eq!(
S2CellIdSnapFunction::level_for_max_snap_radius(Angle::from_radians(
0.999 * radius.radians()
)),
expected,
"0.999 * radius failed for level {level}"
);
}
assert_eq!(
S2CellIdSnapFunction::level_for_max_snap_radius(Angle::from_radians(5.0)),
0
);
assert_eq!(
S2CellIdSnapFunction::level_for_max_snap_radius(Angle::from_radians(1e-30)),
MAX_CELL_LEVEL
);
}
#[test]
fn test_int_latlng_snap_e7() {
let snap = IntLatLngSnapFunction::new(7);
assert_eq!(snap.exponent(), 7);
let p = LatLng::from_degrees(47.1234567, -122.9876543).to_point();
let snapped = snap.snap_point(p);
let ll = LatLng::from_point(snapped);
let lat_e7 = (ll.lat.degrees() * 1e7).round() as i64;
let lng_e7 = (ll.lng.degrees() * 1e7).round() as i64;
assert_eq!(lat_e7, 471234567);
assert_eq!(lng_e7, -1229876543);
}
#[test]
fn test_int_latlng_snap_e5() {
let snap = IntLatLngSnapFunction::new(5);
let p = LatLng::from_degrees(47.12345, -122.98765).to_point();
let snapped = snap.snap_point(p);
let ll = LatLng::from_point(snapped);
let lat_e5 = (ll.lat.degrees() * 1e5).round() as i64;
let lng_e5 = (ll.lng.degrees() * 1e5).round() as i64;
assert_eq!(lat_e5, 4712345);
assert_eq!(lng_e5, -12298765);
}
#[test]
fn test_int_latlng_snap_radius_properties() {
for exp in [0, 3, 5, 6, 7, 10] {
let snap = IntLatLngSnapFunction::new(exp);
let sr = snap.snap_radius();
let vs = snap.min_vertex_separation();
let evs = snap.min_edge_vertex_separation();
assert!(sr.radians() > 0.0, "exp {exp}: sr={}", sr.radians());
assert!(vs.radians() > 0.0, "exp {exp}: vs={}", vs.radians());
assert!(evs.radians() > 0.0, "exp {exp}: evs={}", evs.radians());
assert!(vs.radians() <= 2.0 * sr.radians() + 1e-15);
assert!(evs.radians() <= vs.radians() + 1e-15);
}
}
#[test]
fn test_int_latlng_exponent_for_max_snap_radius() {
for exp in IntLatLngSnapFunction::MIN_EXPONENT..=IntLatLngSnapFunction::MAX_EXPONENT {
let radius = IntLatLngSnapFunction::min_snap_radius_for_exponent(exp);
assert_eq!(
IntLatLngSnapFunction::exponent_for_max_snap_radius(radius),
exp,
"roundtrip failed for exponent {exp}"
);
let expected = (exp + 1).min(IntLatLngSnapFunction::MAX_EXPONENT);
assert_eq!(
IntLatLngSnapFunction::exponent_for_max_snap_radius(Angle::from_radians(
0.999 * radius.radians()
)),
expected,
"0.999 * radius failed for exponent {exp}"
);
}
assert_eq!(
IntLatLngSnapFunction::exponent_for_max_snap_radius(Angle::from_radians(5.0)),
IntLatLngSnapFunction::MIN_EXPONENT
);
assert_eq!(
IntLatLngSnapFunction::exponent_for_max_snap_radius(Angle::from_radians(1e-30)),
IntLatLngSnapFunction::MAX_EXPONENT
);
}
#[test]
fn test_clone_snap() {
let snap = S2CellIdSnapFunction::new(15);
let cloned = snap.clone_snap();
assert!((cloned.snap_radius().radians() - snap.snap_radius().radians()).abs() < 1e-15);
let snap2 = IntLatLngSnapFunction::new(7);
let cloned2 = snap2.clone_snap();
assert!((cloned2.snap_radius().radians() - snap2.snap_radius().radians()).abs() < 1e-15);
}
#[test]
fn test_cell_id_snap_point_roundtrip() {
use crate::s2::coords::MAX_CELL_LEVEL;
use crate::s2::testing::random_cell_id_at_level;
use rand::SeedableRng;
use rand::rngs::StdRng;
let mut rng = StdRng::seed_from_u64(42);
for _iter in 0..1000 {
for level in 0..=MAX_CELL_LEVEL {
let f = S2CellIdSnapFunction::new(level);
let p = random_cell_id_at_level(&mut rng, level).to_point();
assert_eq!(
p,
f.snap_point(p),
"snap_point should preserve cell center at level {level}"
);
}
}
}
#[test]
fn test_int_latlng_snap_point_roundtrip() {
use crate::s2::latlng::LatLng;
use crate::s2::testing::random_point;
use rand::SeedableRng;
use rand::rngs::StdRng;
let mut rng = StdRng::seed_from_u64(43);
for _iter in 0..1000 {
let p = random_point(&mut rng);
let ll = LatLng::from_point(p);
let p5 = LatLng::from_e5(ll.lat.e5(), ll.lng.e5()).to_point();
assert_eq!(
p5,
IntLatLngSnapFunction::new(5).snap_point(p5),
"E5 point should snap to itself"
);
let p6 = LatLng::from_e6(ll.lat.e6(), ll.lng.e6()).to_point();
assert_eq!(
p6,
IntLatLngSnapFunction::new(6).snap_point(p6),
"E6 point should snap to itself"
);
let p7 = LatLng::from_e7(ll.lat.e7(), ll.lng.e7()).to_point();
assert_eq!(
p7,
IntLatLngSnapFunction::new(7).snap_point(p7),
"E7 point should snap to itself"
);
let p7not6 = LatLng::from_e7(10 * ll.lat.e6() + 1, 10 * ll.lng.e6() + 1).to_point();
assert_ne!(
p7not6,
IntLatLngSnapFunction::new(6).snap_point(p7not6),
"E7-only point should not snap to itself at E6"
);
}
}
use crate::s2::cell::Cell;
use crate::s2::edge_distances::distance_from_segment;
use crate::s2::point_measures::turn_angle;
use std::collections::{BTreeMap, BTreeSet};
const RATIO_TOLERANCE: f64 = 1e-2;
fn get_max_vertex_distance_cell(p: Point, id: CellId) -> Angle {
let cell = Cell::from(id);
(0..4)
.map(|i| p.distance(cell.vertex(i)))
.fold(Angle::default(), |a, b| {
if a.radians() > b.radians() { a } else { b }
})
}
fn get_circumradius(a: Point, b: Point, c: Point) -> Angle {
let too_big = Angle::from_radians(std::f64::consts::PI);
let ta = turn_angle(a, b, c).radians();
let ieee_rem = ta - (ta / std::f64::consts::PI).round() * std::f64::consts::PI;
if ieee_rem.abs() < 1e-2 {
return too_big;
}
let a2 = (b - c).0.norm2();
let b2 = (c - a).0.norm2();
let c2 = (a - b).0.norm2();
if a2 > 2.0 || b2 > 2.0 || c2 > 2.0 {
return too_big;
}
let ma = a2 * (b2 + c2 - a2);
let mb = b2 * (c2 + a2 - b2);
let mc = c2 * (a2 + b2 - c2);
let denom = ma + mb + mc;
if denom.abs() < 1e-30 {
return too_big;
}
let px = (ma * a.x() + mb * b.x() + mc * c.x()) / denom;
let py = (ma * a.y() + mb * b.y() + mc * c.y()) / denom;
let pz = (ma * a.z() + mb * b.z() + mc * c.z()) / denom;
let p = Point::from_coords(px, py, pz);
p.distance(a)
}
fn get_neighbors(id: CellId) -> Vec<CellId> {
let mut nbrs: Vec<CellId> = vec![id];
for _layer in 0..2 {
let mut new_nbrs = Vec::new();
for &nbr in &nbrs {
if let Some(n) = nbr.all_neighbors(id.level()) {
new_nbrs.extend(n);
}
}
nbrs.extend(new_nbrs);
nbrs.retain(|&c| c != id);
nbrs.sort_unstable();
nbrs.dedup();
}
nbrs
}
fn update_cell_id_min_vertex_sep(id0: CellId, scores: &mut Vec<(f64, CellId)>) {
let site0 = id0.to_point();
if let Some(nbrs) = id0.all_neighbors(id0.level()) {
for id1 in nbrs {
let site1 = id1.to_point();
let vertex_sep = site0.distance(site1);
let max_snap_radius = get_max_vertex_distance_cell(site0, id1);
if max_snap_radius.radians() <= 0.0 {
continue;
}
let r = vertex_sep.radians() / max_snap_radius.radians();
scores.push((r, id0));
}
}
}
fn get_cell_id_min_vertex_sep(level: u8, best_cells: &mut BTreeSet<CellId>) -> f64 {
let search_focus = CellId::from_face(0).children()[3];
let mut scores: Vec<(f64, CellId)> = Vec::new();
if level == 0 {
update_cell_id_min_vertex_sep(CellId::from_face(0), &mut scores);
} else {
let parents: Vec<CellId> = best_cells.iter().copied().collect();
for parent in parents {
let mut child = parent.child_begin();
let end = parent.child_end();
while child != end {
update_cell_id_min_vertex_sep(child, &mut scores);
child = child.next();
}
}
}
scores.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
scores.dedup();
best_cells.clear();
let num_to_keep = 300;
let mut kept = 0;
for &(_, id) in &scores {
if (search_focus.contains(id) || id.contains(search_focus)) && best_cells.insert(id) {
kept += 1;
if kept >= num_to_keep {
break;
}
}
}
scores.first().map_or(1e10, |s| s.0)
}
#[test]
fn test_cell_id_min_vertex_separation_snap_radius_ratio() {
let mut best_score = 1e10_f64;
let mut best_cells = BTreeSet::new();
for level in 0..=MAX_CELL_LEVEL {
let score = get_cell_id_min_vertex_sep(level, &mut best_cells);
best_score = best_score.min(score);
}
assert!(
(best_score - 0.548_490_277_027_825).abs() < RATIO_TOLERANCE,
"S2CellId min_vertex_sep / snap_radius = {best_score:.15}, expected ~0.548490"
);
}
fn get_cell_id_min_edge_sep<F>(
label: &str,
objective: F,
level: u8,
best_cells: &mut BTreeSet<CellId>,
) -> f64
where
F: Fn(u8, Angle, Angle, Angle) -> f64,
{
let search_focus = CellId::from_face(0).children()[3];
let mut best_scores: BTreeMap<CellId, f64> = BTreeMap::new();
let parents: Vec<CellId> = best_cells.iter().copied().collect();
for parent in parents {
let mut id0 = parent.child_begin_at_level(level);
let end = parent.child_end_at_level(level);
while id0 != end {
let site0 = id0.to_point();
let nbrs = get_neighbors(id0);
for &id1 in &nbrs {
let site1 = id1.to_point();
let max_v1 = get_max_vertex_distance_cell(site0, id1);
for &id2 in &nbrs {
if id2 <= id1 {
continue;
}
let site2 = id2.to_point();
let min_snap_radius = get_circumradius(site0, site1, site2);
if min_snap_radius.radians() > MAX_SNAP_RADIUS.radians() {
continue;
}
let max_v2 = get_max_vertex_distance_cell(site0, id2);
let max_snap_radius = if max_v1.radians() < max_v2.radians() {
max_v1
} else {
max_v2
};
if min_snap_radius.radians() > max_snap_radius.radians() {
continue;
}
let edge_sep = distance_from_segment(site0, site1, site2);
let score = objective(level, edge_sep, min_snap_radius, max_snap_radius);
let entry = best_scores.entry(id0).or_insert(score);
if score < *entry {
*entry = score;
}
}
}
id0 = id0.next();
}
}
let mut sorted: Vec<(f64, CellId)> = best_scores
.iter()
.map(|(&id, &score)| (score, id))
.collect();
sorted.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
best_cells.clear();
let num_to_keep = 100;
let mut kept = 0;
for &(_, id) in &sorted {
let mut to_add = vec![id];
if let Some(n) = id.all_neighbors(id.level()) {
to_add.extend(n);
}
for nbr in to_add {
if (search_focus.contains(nbr) || nbr.contains(search_focus))
&& best_cells.insert(nbr)
{
kept += 1;
if kept >= num_to_keep {
let _ = label;
return sorted.first().map_or(1e10, |s| s.0);
}
}
}
}
let _ = label;
sorted.first().map_or(1e10, |s| s.0)
}
fn get_cell_id_min_edge_sep_to_level<F>(label: &str, objective: F, max_level: u8) -> f64
where
F: Fn(u8, Angle, Angle, Angle) -> f64,
{
let mut best_score = 1e10_f64;
let mut best_cells = BTreeSet::new();
best_cells.insert(CellId::from_face(0));
for level in 0..=max_level {
let score = get_cell_id_min_edge_sep(label, &objective, level, &mut best_cells);
best_score = best_score.min(score);
}
best_score
}
fn get_cell_id_min_edge_sep_all_levels<F>(label: &str, objective: F) -> f64
where
F: Fn(u8, Angle, Angle, Angle) -> f64,
{
get_cell_id_min_edge_sep_to_level(label, objective, MAX_CELL_LEVEL)
}
#[test]
#[ignore = "slow worst-case ratio search (~5-10s)"]
fn test_cell_id_min_edge_vertex_separation_for_level() {
let score = get_cell_id_min_edge_sep_all_levels(
"min_sep_for_level",
|level, edge_sep, _min_sr, _max_sr| {
let min_diag = metric::MIN_DIAG.value(level);
edge_sep.radians() / min_diag
},
);
assert!(
(score - 0.397_359_568_667_803).abs() < RATIO_TOLERANCE,
"S2CellId min_edge_vertex_sep / kMinDiag = {score:.15}, expected ~0.397360"
);
}
#[test]
#[ignore = "slow worst-case ratio search (~5-10s)"]
fn test_cell_id_min_edge_vertex_separation_at_min_snap_radius() {
let score = get_cell_id_min_edge_sep_all_levels(
"min_sep_at_min_radius",
|level, edge_sep, min_sr, _max_sr| {
let min_radius_at_level = metric::MAX_DIAG.value(level) / 2.0;
if min_sr.radians() <= (1.0 + 1e-10) * min_radius_at_level {
edge_sep.radians() / metric::MIN_DIAG.value(level)
} else {
100.0
}
},
);
assert!(
(score - 0.565_298_006_776_224).abs() < RATIO_TOLERANCE,
"S2CellId min_edge_vertex_sep/kMinDiag at min radius = {score:.15}, expected ~0.565298"
);
}
#[test]
#[ignore = "slow worst-case ratio search (~5-10s)"]
fn test_cell_id_min_edge_vertex_separation_snap_radius_ratio() {
let score = get_cell_id_min_edge_sep_all_levels(
"min_sep_snap_radius_ratio",
|_level, edge_sep, _min_sr, max_sr| edge_sep.radians() / max_sr.radians(),
);
assert!(
(score - 0.219_666_695_288_891).abs() < RATIO_TOLERANCE,
"S2CellId min_edge_vertex_sep / snap_radius = {score:.15}, expected ~0.219667"
);
}
#[test]
fn test_cell_id_min_edge_vertex_separation_search_smoke() {
const SHALLOW_LEVEL: u8 = 4;
let for_level = get_cell_id_min_edge_sep_to_level(
"min_sep_for_level",
|level, edge_sep, _min_sr, _max_sr| edge_sep.radians() / metric::MIN_DIAG.value(level),
SHALLOW_LEVEL,
);
assert!(
(0.39..1.0).contains(&for_level),
"shallow S2CellId edge_sep/min_diag = {for_level:.6}, expected in [0.39, 1.0)"
);
let ratio = get_cell_id_min_edge_sep_to_level(
"min_sep_snap_radius_ratio",
|_level, edge_sep, _min_sr, max_sr| edge_sep.radians() / max_sr.radians(),
SHALLOW_LEVEL,
);
assert!(
(0.21..1.0).contains(&ratio),
"shallow S2CellId edge_sep/snap_radius = {ratio:.6}, expected in [0.21, 1.0)"
);
}
type IntLL = (i64, i64);
fn intll_is_valid(ll: IntLL, scale: i64) -> bool {
ll.0.abs() <= scale / 2 && ll.1.abs() <= scale
}
fn intll_has_valid_vertices(ll: IntLL, scale: i64) -> bool {
ll.0.abs() < scale / 2 && ll.1.abs() < scale
}
fn intll_rescale(ll: IntLL, factor: f64) -> IntLL {
(
(ll.0 as f64 * factor).round() as i64,
(ll.1 as f64 * factor).round() as i64,
)
}
fn intll_to_point(ll: IntLL, scale: i64) -> Point {
LatLng::from_radians(
ll.0 as f64 * (std::f64::consts::PI / scale as f64),
ll.1 as f64 * (std::f64::consts::PI / scale as f64),
)
.to_point()
}
fn intll_get_vertex(ll: IntLL, scale: i64, i: usize) -> Point {
let dlat: i64 = if i == 0 || i == 3 { -1 } else { 1 };
let dlng: i64 = if i == 0 || i == 1 { -1 } else { 1 };
let doubled = (2 * ll.0 + dlat, 2 * ll.1 + dlng);
intll_to_point(doubled, 2 * scale)
}
fn intll_max_vertex_dist(p: Point, ll: IntLL, scale: i64) -> Angle {
(0..4)
.map(|i| p.distance(intll_get_vertex(ll, scale, i)))
.fold(Angle::default(), |a, b| {
if a.radians() > b.radians() { a } else { b }
})
}
fn get_latlng_min_vertex_sep(
old_scale: i64,
scale: i64,
best_configs: &mut BTreeSet<IntLL>,
) -> f64 {
let min_snap_radius_at_scale = Angle::from_radians(
std::f64::consts::FRAC_1_SQRT_2 * std::f64::consts::PI / scale as f64,
);
let mut scores: Vec<(f64, IntLL)> = Vec::new();
let scale_factor = scale as f64 / old_scale as f64;
let parents: Vec<IntLL> = best_configs.iter().copied().collect();
for parent in parents {
let new_parent = intll_rescale(parent, scale_factor);
for dlat0 in -7..=7 {
let ll0 = (new_parent.0 + dlat0, new_parent.1);
if !intll_is_valid(ll0, scale) || ll0.0 < 0 {
continue;
}
let site0 = intll_to_point(ll0, scale);
for dlat1 in 0..=2 {
for dlng1 in 0..=5 {
let ll1 = (ll0.0 + dlat1, ll0.1 + dlng1);
if ll1 == ll0 || !intll_has_valid_vertices(ll1, scale) {
continue;
}
let max_snap_radius = intll_max_vertex_dist(site0, ll1, scale);
if max_snap_radius.radians() < min_snap_radius_at_scale.radians() {
continue;
}
let site1 = intll_to_point(ll1, scale);
let vertex_sep = site0.distance(site1);
let r = vertex_sep.radians() / max_snap_radius.radians();
scores.push((r, ll0));
}
}
}
}
scores.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
scores.dedup();
best_configs.clear();
let num_to_keep = 100;
let mut kept = 0;
for &(_, ll) in &scores {
if best_configs.insert(ll) {
kept += 1;
if kept >= num_to_keep {
break;
}
}
}
scores.first().map_or(1e10, |s| s.0)
}
#[test]
fn test_intlatlng_min_vertex_separation_snap_radius_ratio() {
let mut best_score = 1e10_f64;
let mut best_configs = BTreeSet::new();
let mut scale: i64 = 18;
for lat0 in 0..=9 {
best_configs.insert((lat0, 0_i64));
}
for _exp in 0..=10 {
let score = get_latlng_min_vertex_sep(scale, 10 * scale, &mut best_configs);
best_score = best_score.min(score);
scale *= 10;
}
assert!(
(best_score - 0.471_337_477_576_603).abs() < RATIO_TOLERANCE,
"IntLatLng min_vertex_sep / snap_radius = {best_score:.15}, expected ~0.471337"
);
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct LatLngConfig {
scale: i64,
ll0: IntLL,
ll1: IntLL,
ll2: IntLL,
}
fn get_latlng_min_edge_sep<F>(
label: &str,
objective: &F,
scale: i64,
best_configs: &mut Vec<LatLngConfig>,
) -> f64
where
F: Fn(i64, Angle, Angle) -> f64,
{
let min_snap_radius_at_scale = Angle::from_radians(
std::f64::consts::FRAC_1_SQRT_2 * std::f64::consts::PI / scale as f64,
);
let mut scores: Vec<(f64, LatLngConfig)> = Vec::new();
for parent in best_configs.iter() {
let sf = scale as f64 / parent.scale as f64;
let pll0 = intll_rescale(parent.ll0, sf);
let pll1 = intll_rescale(parent.ll1, sf);
let pll2 = intll_rescale(parent.ll2, sf);
for dlat0 in -1..=1_i64 {
let ll0 = (pll0.0 + dlat0, pll0.1);
if !intll_is_valid(ll0, scale) || ll0.0 < 0 {
continue;
}
let site0 = intll_to_point(ll0, scale);
for dlat1 in -1..=1_i64 {
for dlng1 in -2..=2_i64 {
let ll1 = (pll1.0 + dlat0 + dlat1, pll1.1 + dlng1);
if ll1 == ll0 || !intll_has_valid_vertices(ll1, scale) {
continue;
}
if (ll1.0 - ll0.0).abs() > 2 {
continue;
}
let site1 = intll_to_point(ll1, scale);
let max_v1 = intll_max_vertex_dist(site0, ll1, scale);
for dlat2 in -1..=1_i64 {
for dlng2 in -2..=2_i64 {
let ll2 = (pll2.0 + dlat0 + dlat2, pll2.1 + dlng2);
if !intll_has_valid_vertices(ll2, scale) {
continue;
}
if (ll2.0 - ll0.0).abs() > 2 {
continue;
}
if ll2 <= ll1 || ll2.1 < 0 {
continue;
}
let site2 = intll_to_point(ll2, scale);
let min_snap_radius = get_circumradius(site0, site1, site2);
if min_snap_radius.radians() > MAX_SNAP_RADIUS.radians() {
continue;
}
let max_v2 = intll_max_vertex_dist(site0, ll2, scale);
let max_snap_radius = if max_v1.radians() < max_v2.radians() {
max_v1
} else {
max_v2
};
if min_snap_radius.radians() > max_snap_radius.radians() {
continue;
}
if max_snap_radius.radians() < min_snap_radius_at_scale.radians() {
continue;
}
let edge_sep = distance_from_segment(site0, site1, site2);
let score = objective(scale, edge_sep, max_snap_radius);
scores.push((
score,
LatLngConfig {
scale,
ll0,
ll1,
ll2,
},
));
}
}
}
}
}
}
scores.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
scores.dedup_by(|a, b| a.1 == b.1);
let num_to_keep = 500;
*best_configs = scores.iter().take(num_to_keep).map(|s| s.1).collect();
let _ = label;
scores.first().map_or(1e10, |s| s.0)
}
fn get_latlng_min_edge_sep_steps<F>(label: &str, objective: F, num_exp_steps: usize) -> f64
where
F: Fn(i64, Angle, Angle) -> f64,
{
let mut best_score = 1e10_f64;
let mut best_configs: Vec<LatLngConfig> = Vec::new();
let initial_scale: i64 = 6;
let max_lng = initial_scale;
let max_lat = initial_scale / 2;
for lat0 in 0..=max_lat {
for lat1 in (lat0 - 2)..=(max_lat.min(lat0 + 2)) {
for lng1 in 0..=max_lng {
for lat2 in lat1..=(max_lat.min(lat0 + 2)) {
for lng2 in 0..=max_lng {
let ll0 = (lat0, 0_i64);
let ll1 = (lat1, lng1);
let ll2 = (lat2, lng2);
if ll2 <= ll1 {
continue;
}
best_configs.push(LatLngConfig {
scale: initial_scale,
ll0,
ll1,
ll2,
});
}
}
}
}
}
let mut scale = initial_scale;
let mut target_scale: i64 = 180;
for _exp in 0..num_exp_steps {
while scale < target_scale {
scale = (scale as f64 * 1.8).min(target_scale as f64) as i64;
let score = get_latlng_min_edge_sep(label, &objective, scale, &mut best_configs);
if scale == target_scale {
best_score = best_score.min(score);
}
}
target_scale *= 10;
}
best_score
}
fn get_latlng_min_edge_sep_all<F>(label: &str, objective: F) -> f64
where
F: Fn(i64, Angle, Angle) -> f64,
{
get_latlng_min_edge_sep_steps(label, objective, 11)
}
#[test]
#[ignore = "slow worst-case ratio search (~5-10s)"]
fn test_intlatlng_min_edge_vertex_separation_for_level() {
let score = get_latlng_min_edge_sep_all("min_sep_for_level", |scale, edge_sep, _max_sr| {
let e_unit = std::f64::consts::PI / scale as f64;
edge_sep.radians() / e_unit
});
assert!(
(score - 0.277_258_917_722_462).abs() < RATIO_TOLERANCE,
"IntLatLng min_edge_vertex_sep / e_unit = {score:.15}, expected ~0.277259"
);
}
#[test]
#[ignore = "slow worst-case ratio search (~5-10s)"]
fn test_intlatlng_min_edge_vertex_separation_snap_radius_ratio() {
let score =
get_latlng_min_edge_sep_all("min_sep_snap_radius_ratio", |_scale, edge_sep, max_sr| {
edge_sep.radians() / max_sr.radians()
});
assert!(
(score - 0.222_222_126_756_717).abs() < RATIO_TOLERANCE,
"IntLatLng min_edge_vertex_sep / snap_radius = {score:.15}, expected ~0.222222"
);
}
#[test]
fn test_intlatlng_min_edge_vertex_separation_search_smoke() {
const SHALLOW_STEPS: usize = 1;
let for_level = get_latlng_min_edge_sep_steps(
"min_sep_for_level",
|scale, edge_sep, _max_sr| edge_sep.radians() / (std::f64::consts::PI / scale as f64),
SHALLOW_STEPS,
);
assert!(
(0.27..1.0).contains(&for_level),
"shallow IntLatLng edge_sep/e_unit = {for_level:.6}, expected in [0.27, 1.0)"
);
let ratio = get_latlng_min_edge_sep_steps(
"min_sep_snap_radius_ratio",
|_scale, edge_sep, max_sr| edge_sep.radians() / max_sr.radians(),
SHALLOW_STEPS,
);
assert!(
(0.21..1.0).contains(&ratio),
"shallow IntLatLng edge_sep/snap_radius = {ratio:.6}, expected in [0.21, 1.0)"
);
}
fn make_test_point(x: i32, y: i32, z: i32) -> Option<Point> {
let (xf, yf, zf) = (f64::from(x), f64::from(y), f64::from(z));
let norm = (xf * xf + yf * yf + zf * zf).sqrt();
if norm < 1e-10 {
return None;
}
Some(Point::from_coords(xf / norm, yf / norm, zf / norm))
}
#[quickcheck]
fn prop_identity_snap_is_identity(x: i32, y: i32, z: i32) -> bool {
let Some(p) = make_test_point(x, y, z) else {
return true;
};
let snap = IdentitySnapFunction::new(Angle::from_degrees(1.0));
snap.snap_point(p) == p
}
#[quickcheck]
fn prop_identity_separation_equals_radius(r_deg: u32) -> bool {
let r = f64::from(r_deg % 70); let snap = IdentitySnapFunction::new(Angle::from_degrees(r));
(snap.min_vertex_separation().radians() - snap.snap_radius().radians()).abs() < 1e-15
}
#[quickcheck]
fn prop_identity_edge_sep_half_radius(r_deg: u32) -> bool {
let r = f64::from(r_deg % 70);
let snap = IdentitySnapFunction::new(Angle::from_degrees(r));
(snap.min_edge_vertex_separation().radians() - 0.5 * snap.snap_radius().radians()).abs()
< 1e-15
}
#[quickcheck]
fn prop_cell_id_snap_idempotent(x: i32, y: i32, z: i32, level: u8) -> bool {
let Some(p) = make_test_point(x, y, z) else {
return true;
};
let level = level % (MAX_CELL_LEVEL + 1);
let snap = S2CellIdSnapFunction::new(level);
let snapped = snap.snap_point(p);
snap.snap_point(snapped) == snapped
}
#[quickcheck]
fn prop_cell_id_snap_is_cell_center(x: i32, y: i32, z: i32, level: u8) -> bool {
let Some(p) = make_test_point(x, y, z) else {
return true;
};
let level = level % (MAX_CELL_LEVEL + 1);
let snap = S2CellIdSnapFunction::new(level);
let snapped = snap.snap_point(p);
let expected = CellId::from_point(&p).parent_at_level(level).to_point();
snapped == expected
}
#[quickcheck]
fn prop_cell_id_snap_within_radius(x: i32, y: i32, z: i32, level: u8) -> bool {
let Some(p) = make_test_point(x, y, z) else {
return true;
};
let level = level % (MAX_CELL_LEVEL + 1);
let snap = S2CellIdSnapFunction::new(level);
let snapped = snap.snap_point(p);
let dist = p.distance(snapped);
dist.radians() <= snap.snap_radius().radians() + 1e-15
}
#[quickcheck]
fn prop_cell_id_separation_ordering(level: u8) -> bool {
let level = level % (MAX_CELL_LEVEL + 1);
let snap = S2CellIdSnapFunction::new(level);
let sr = snap.snap_radius().radians();
let vs = snap.min_vertex_separation().radians();
let evs = snap.min_edge_vertex_separation().radians();
vs > 0.0 && evs > 0.0 && evs <= vs + 1e-15 && vs <= 2.0 * sr + 1e-15
}
#[quickcheck]
fn prop_cell_id_min_snap_radius_monotone(level: u8) -> bool {
let level = level % MAX_CELL_LEVEL; let r_low = S2CellIdSnapFunction::min_snap_radius_for_level(level);
let r_high = S2CellIdSnapFunction::min_snap_radius_for_level(level + 1);
r_low.radians() >= r_high.radians()
}
#[quickcheck]
fn prop_cell_id_distinct_points_separated(
x1: i32,
y1: i32,
z1: i32,
x2: i32,
y2: i32,
z2: i32,
level: u8,
) -> bool {
let (Some(p1), Some(p2)) = (make_test_point(x1, y1, z1), make_test_point(x2, y2, z2))
else {
return true;
};
let level = level % (MAX_CELL_LEVEL + 1);
let snap = S2CellIdSnapFunction::new(level);
let s1 = snap.snap_point(p1);
let s2 = snap.snap_point(p2);
if s1 == s2 {
return true; }
let dist = s1.distance(s2).radians();
let min_sep = snap.min_vertex_separation().radians();
dist >= min_sep - 1e-15
}
#[quickcheck]
fn prop_int_latlng_snap_idempotent(x: i32, y: i32, z: i32, exp: u8) -> bool {
let Some(p) = make_test_point(x, y, z) else {
return true;
};
let exp = i32::from(exp) % (IntLatLngSnapFunction::MAX_EXPONENT + 1);
let snap = IntLatLngSnapFunction::new(exp);
let snapped = snap.snap_point(p);
let snapped2 = snap.snap_point(snapped);
snapped.distance(snapped2).radians() < 1e-12
}
#[quickcheck]
fn prop_int_latlng_snap_coordinates_integral(x: i32, y: i32, z: i32, exp: u8) -> bool {
let Some(p) = make_test_point(x, y, z) else {
return true;
};
let exp = i32::from(exp) % (IntLatLngSnapFunction::MAX_EXPONENT + 1);
let snap = IntLatLngSnapFunction::new(exp);
let snapped = snap.snap_point(p);
let ll = LatLng::from_point(snapped);
let power = 10_f64.powi(exp);
let lat_int = (ll.lat.degrees() * power).round();
let lng_int = (ll.lng.degrees() * power).round();
let lat_back = lat_int / power;
let lng_back = lng_int / power;
(ll.lat.degrees() - lat_back).abs() < 1e-10 && (ll.lng.degrees() - lng_back).abs() < 1e-10
}
#[quickcheck]
fn prop_int_latlng_snap_within_radius(x: i32, y: i32, z: i32, exp: u8) -> bool {
let Some(p) = make_test_point(x, y, z) else {
return true;
};
let exp = i32::from(exp) % (IntLatLngSnapFunction::MAX_EXPONENT + 1);
let snap = IntLatLngSnapFunction::new(exp);
let snapped = snap.snap_point(p);
let dist = p.distance(snapped);
dist.radians() <= snap.snap_radius().radians() + 1e-15
}
#[quickcheck]
fn prop_int_latlng_separation_ordering(exp: u8) -> bool {
let exp = i32::from(exp) % (IntLatLngSnapFunction::MAX_EXPONENT + 1);
let snap = IntLatLngSnapFunction::new(exp);
let sr = snap.snap_radius().radians();
let vs = snap.min_vertex_separation().radians();
let evs = snap.min_edge_vertex_separation().radians();
vs > 0.0 && evs > 0.0 && evs <= vs + 1e-15 && vs <= 2.0 * sr + 1e-15
}
#[quickcheck]
fn prop_int_latlng_min_snap_radius_monotone(exp: u8) -> bool {
let exp = i32::from(exp) % IntLatLngSnapFunction::MAX_EXPONENT; let r_low = IntLatLngSnapFunction::min_snap_radius_for_exponent(exp);
let r_high = IntLatLngSnapFunction::min_snap_radius_for_exponent(exp + 1);
r_low.radians() >= r_high.radians()
}
#[quickcheck]
fn prop_level_for_max_snap_radius_monotone(a_deg: u32, b_deg: u32) -> bool {
let r1 = Angle::from_degrees(0.001 * (f64::from(a_deg % 70_000) + 0.001));
let r2 = Angle::from_degrees(0.001 * (f64::from(b_deg % 70_000) + 0.001));
let l1 = S2CellIdSnapFunction::level_for_max_snap_radius(r1);
let l2 = S2CellIdSnapFunction::level_for_max_snap_radius(r2);
if r1.radians() >= r2.radians() {
l1 <= l2 } else {
l2 <= l1
}
}
#[quickcheck]
fn prop_exponent_for_max_snap_radius_monotone(a_deg: u32, b_deg: u32) -> bool {
let r1 = Angle::from_degrees(0.001 * (f64::from(a_deg % 70_000) + 0.001));
let r2 = Angle::from_degrees(0.001 * (f64::from(b_deg % 70_000) + 0.001));
let e1 = IntLatLngSnapFunction::exponent_for_max_snap_radius(r1);
let e2 = IntLatLngSnapFunction::exponent_for_max_snap_radius(r2);
if r1.radians() >= r2.radians() {
e1 <= e2
} else {
e2 <= e1
}
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_identity_snap_roundtrip() {
let sf = IdentitySnapFunction::new(Angle::from_degrees(1.0));
let json = serde_json::to_string(&sf).unwrap();
let back: IdentitySnapFunction = serde_json::from_str(&json).unwrap();
assert_eq!(sf.snap_radius(), back.snap_radius());
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_cell_id_snap_roundtrip() {
let sf = S2CellIdSnapFunction::new(10);
let json = serde_json::to_string(&sf).unwrap();
let back: S2CellIdSnapFunction = serde_json::from_str(&json).unwrap();
assert_eq!(sf.snap_radius(), back.snap_radius());
assert_eq!(sf.level(), back.level());
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_int_latlng_snap_roundtrip() {
let sf = IntLatLngSnapFunction::new(7);
let json = serde_json::to_string(&sf).unwrap();
let back: IntLatLngSnapFunction = serde_json::from_str(&json).unwrap();
assert_eq!(sf.exponent(), back.exponent());
assert!((sf.snap_radius().radians() - back.snap_radius().radians()).abs() < 1e-20);
}
}
#[cfg(test)]
#[path = "snap_tests.rs"]
mod snap_tests;