use s2rst::s2::{LatLng, Loop, Polygon};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Manifold {
Planar,
Spherical,
Geodesic,
}
impl Manifold {
pub fn from_flag(s: &str) -> Option<Manifold> {
match s {
"planar" => Some(Manifold::Planar),
"spherical" => Some(Manifold::Spherical),
"geodesic" => Some(Manifold::Geodesic),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Manifold::Planar => "planar",
Manifold::Spherical => "spherical",
Manifold::Geodesic => "geodesic",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeometryError {
message: String,
degenerate_operand: bool,
}
impl GeometryError {
pub(crate) fn new(message: impl Into<String>) -> Self {
GeometryError {
message: message.into(),
degenerate_operand: false,
}
}
pub fn is_degenerate_operand(&self) -> bool {
self.degenerate_operand
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for GeometryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "geometry kernel error: {}", self.message)
}
}
impl std::error::Error for GeometryError {}
pub fn intersect_polygon(
a: &[(f64, f64)],
b: &[(f64, f64)],
manifold: Manifold,
) -> Result<Vec<(f64, f64)>, GeometryError> {
if manifold == Manifold::Planar && planar_bbox_disjoint(a, b) {
return Ok(Vec::new());
}
let a = as_ring(a, "poly_a")?;
let b = as_ring(b, "poly_b")?;
match manifold {
Manifold::Spherical | Manifold::Geodesic => intersect_spherical(&a, &b),
Manifold::Planar => Ok(intersect_planar_convex(&a, &b)),
}
}
pub(crate) fn planar_ring_bbox(ring: &[(f64, f64)]) -> Option<[f64; 4]> {
let (&(x0, y0), rest) = ring.split_first()?;
let (mut xmin, mut xmax, mut ymin, mut ymax) = (x0, x0, y0, y0);
for &(x, y) in rest {
if x < xmin {
xmin = x;
}
if x > xmax {
xmax = x;
}
if y < ymin {
ymin = y;
}
if y > ymax {
ymax = y;
}
}
Some([xmin, xmax, ymin, ymax])
}
pub(crate) fn bboxes_disjoint(a: &[f64; 4], b: &[f64; 4]) -> bool {
a[1] < b[0] || b[1] < a[0] || a[3] < b[2] || b[3] < a[2]
}
pub(crate) fn planar_bbox_disjoint(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
match (planar_ring_bbox(a), planar_ring_bbox(b)) {
(Some(ba), Some(bb)) => bboxes_disjoint(&ba, &bb),
_ => true,
}
}
fn dedup_consecutive(ring: &[(f64, f64)]) -> Vec<(f64, f64)> {
let n = ring.len();
if n <= 1 {
return ring.to_vec();
}
let mut out: Vec<(f64, f64)> = Vec::with_capacity(n);
out.push(ring[0]);
for &pt in &ring[1..] {
let last = *out.last().unwrap();
if !allclose_pt(pt, last) {
out.push(pt);
}
}
if out.len() >= 2 && allclose_pt(out[0], *out.last().unwrap()) {
out.pop();
}
out
}
fn as_ring(poly: &[(f64, f64)], who: &str) -> Result<Vec<(f64, f64)>, GeometryError> {
let arr = dedup_consecutive(poly);
if arr.len() < 3 {
return Err(GeometryError {
degenerate_operand: true,
..GeometryError::new(format!(
"intersect_polygon {who} needs >=3 distinct vertices, got {}",
arr.len()
))
});
}
Ok(arr)
}
fn s2_polygon(ring: &[(f64, f64)]) -> Result<Polygon, GeometryError> {
let pts = ring
.iter()
.map(|&(lon, lat)| LatLng::from_degrees(lat, lon).to_point())
.collect();
let mut l = Loop::new(pts);
l.normalize();
let p = Polygon::from_loops(vec![l]);
p.validate()
.map_err(|e| GeometryError::new(format!("degenerate spherical ring: {e}")))?;
Ok(p)
}
fn shell_vertices(p: &Polygon) -> Vec<(f64, f64)> {
let mut verts = Vec::new();
for l in p.loops() {
if l.is_hole() {
continue;
}
for v in l.vertices() {
let ll = LatLng::from_point(*v);
verts.push((ll.lng.degrees(), ll.lat.degrees()));
}
}
verts
}
fn intersect_spherical(
a: &[(f64, f64)],
b: &[(f64, f64)],
) -> Result<Vec<(f64, f64)>, GeometryError> {
let mut pa = s2_polygon(a)?;
let mut pb = s2_polygon(b)?;
let clip = Polygon::intersection(&mut pa, &mut pb);
if clip.is_empty_polygon() {
return Ok(Vec::new());
}
Ok(shell_vertices(&clip))
}
fn intersect_planar_convex(subject: &[(f64, f64)], clip: &[(f64, f64)]) -> Vec<(f64, f64)> {
if subject.len() < 3 || clip.len() < 3 {
return Vec::new();
}
let clip = orient_ccw(clip);
let mut output: Vec<(f64, f64)> = subject.to_vec();
for i in 0..clip.len() {
if output.is_empty() {
break;
}
let edge_a = clip[i];
let edge_b = clip[(i + 1) % clip.len()];
let input = std::mem::take(&mut output);
let n = input.len();
for j in 0..n {
let cur = input[j];
let prev = input[(j + n - 1) % n];
let cur_in = is_left_of(edge_a, edge_b, cur);
let prev_in = is_left_of(edge_a, edge_b, prev);
if cur_in {
if !prev_in && let Some(ip) = line_segment_clip(prev, cur, edge_a, edge_b) {
output.push(ip);
}
output.push(cur);
} else if prev_in && let Some(ip) = line_segment_clip(prev, cur, edge_a, edge_b) {
output.push(ip);
}
}
}
dedup_consecutive(&output)
}
pub fn shoelace_signed_area(ring: &[(f64, f64)]) -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let mut acc = 0.0;
for i in 0..n {
let (x0, y0) = ring[i];
let (x1, y1) = ring[(i + 1) % n];
acc += x0 * y1 - x1 * y0;
}
0.5 * acc
}
pub fn shoelace_area(ring: &[(f64, f64)]) -> f64 {
shoelace_signed_area(ring).abs()
}
pub fn spherical_area(ring: &[(f64, f64)]) -> Result<f64, GeometryError> {
let ring = dedup_consecutive(ring);
if ring.len() < 3 {
return Ok(0.0);
}
Ok(s2_polygon(&ring)?.area())
}
pub fn polygon_area(ring: &[(f64, f64)], manifold: Manifold) -> Result<f64, GeometryError> {
match manifold {
Manifold::Planar => Ok(shoelace_area(ring)),
Manifold::Spherical | Manifold::Geodesic => spherical_area(ring),
}
}
pub const DEFAULT_LAT_ATOL: f64 = 1e-9;
pub fn densify_parallel_edges(
ring: &[(f64, f64)],
max_segment_deg: f64,
lat_atol: f64,
) -> Result<Vec<(f64, f64)>, GeometryError> {
if max_segment_deg <= 0.0 || max_segment_deg.is_nan() {
return Err(GeometryError::new(format!(
"densify_parallel_edges max_segment_deg must be positive, got {max_segment_deg}"
)));
}
let r = as_distinct_ring(ring)?;
let n = r.len();
let mut out: Vec<(f64, f64)> = Vec::with_capacity(n);
for i in 0..n {
let (ax, ay) = r[i];
let (bx, by) = r[(i + 1) % n];
out.push((ax, ay));
let dlon = bx - ax;
if (ay - by).abs() <= lat_atol && dlon.abs() > max_segment_deg {
let nseg = (dlon.abs() / max_segment_deg).ceil() as usize;
for k in 1..nseg {
let t = k as f64 / nseg as f64;
out.push((ax + t * dlon, ay + t * (by - ay)));
}
}
}
Ok(out)
}
fn allclose_pt(a: (f64, f64), b: (f64, f64)) -> bool {
(a.0 - b.0).abs() <= 1e-8 + 1e-5 * b.0.abs() && (a.1 - b.1).abs() <= 1e-8 + 1e-5 * b.1.abs()
}
fn as_distinct_ring(ring: &[(f64, f64)]) -> Result<&[(f64, f64)], GeometryError> {
let r = if ring.len() >= 2 && allclose_pt(ring[0], ring[ring.len() - 1]) {
&ring[..ring.len() - 1]
} else {
ring
};
if r.len() < 3 {
return Err(GeometryError::new(format!(
"densify_parallel_edges ring needs ≥3 distinct vertices, got {}",
r.len()
)));
}
Ok(r)
}
pub const SLIVER_ATOL_FACTOR: f64 = 1e-15;
pub fn sliver_atol(radius: f64) -> f64 {
SLIVER_ATOL_FACTOR * radius * radius
}
pub fn area_tolerance_ok(area_x: f64, area_ref: f64, rtol: f64, radius: f64) -> bool {
let atol = sliver_atol(radius);
let a_x = if area_x.abs() <= atol { 0.0 } else { area_x };
let a_ref = if area_ref.abs() <= atol {
0.0
} else {
area_ref
};
(a_x - a_ref).abs() <= atol + rtol * a_ref.abs()
}
fn orient_ccw(ring: &[(f64, f64)]) -> Vec<(f64, f64)> {
if shoelace_signed_area(ring) < 0.0 {
ring.iter().rev().copied().collect()
} else {
ring.to_vec()
}
}
fn is_left_of(a: (f64, f64), b: (f64, f64), p: (f64, f64)) -> bool {
let cross = (b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0);
cross >= 0.0
}
fn line_segment_clip(
p: (f64, f64),
q: (f64, f64),
a: (f64, f64),
b: (f64, f64),
) -> Option<(f64, f64)> {
let r = (q.0 - p.0, q.1 - p.1);
let s = (b.0 - a.0, b.1 - a.1);
let denom = r.0 * s.1 - r.1 * s.0;
if denom == 0.0 {
return None;
}
let t = ((a.0 - p.0) * s.1 - (a.1 - p.1) * s.0) / denom;
Some((p.0 + t * r.0, p.1 + t * r.1))
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-9;
#[test]
fn manifold_flag_round_trips() {
for s in ["planar", "spherical", "geodesic"] {
assert_eq!(Manifold::from_flag(s).unwrap().as_str(), s);
}
assert!(Manifold::from_flag("ellipsoidal").is_none());
assert!(Manifold::from_flag("").is_none());
}
#[test]
fn planar_clip_of_overlapping_unit_squares() {
let a = [(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)];
let b = [(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)];
let ring = intersect_polygon(&a, &b, Manifold::Planar).expect("planar clip");
assert!(ring.len() >= 3, "expected a non-degenerate overlap ring");
assert!(
(shoelace_area(&ring) - 1.0).abs() < TOL,
"area was {}",
shoelace_area(&ring)
);
}
#[test]
fn planar_clip_of_disjoint_squares_is_empty() {
let a = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
let b = [(5.0, 5.0), (6.0, 5.0), (6.0, 6.0), (5.0, 6.0)];
let ring = intersect_polygon(&a, &b, Manifold::Planar).expect("planar clip");
assert!(ring.is_empty());
assert_eq!(shoelace_area(&ring), 0.0);
}
#[test]
fn planar_clip_winding_insensitive() {
let a = [(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)];
let b_cw = [(1.0, 1.0), (1.0, 3.0), (3.0, 3.0), (3.0, 1.0)];
let ring = intersect_polygon(&a, &b_cw, Manifold::Planar).expect("planar clip");
assert!((shoelace_area(&ring) - 1.0).abs() < TOL);
}
#[test]
fn spherical_clip_of_octant_sectors_matches_analytic() {
let a = [(0.0, 0.0), (90.0, 0.0), (0.0, 90.0)];
let b = [(45.0, 0.0), (135.0, 0.0), (45.0, 90.0)];
let ring = intersect_polygon(&a, &b, Manifold::Spherical).expect("spherical clip");
assert!(
ring.len() >= 3,
"expected a non-empty spherical overlap ring"
);
let area = spherical_area(&ring).expect("spherical area");
assert!(
(area - std::f64::consts::FRAC_PI_4).abs() < 1e-9,
"spherical overlap area was {area}, expected π/4"
);
}
#[test]
fn planar_clip_accepts_padded_rings() {
let a = [
(0.5, 0.5),
(2.5, 0.5),
(2.5, 2.5),
(0.5, 2.5),
(0.5, 2.5), ];
let b = [
(1.5, 1.5),
(3.5, 1.5),
(3.5, 3.5),
(1.5, 3.5),
(1.5, 3.5), (1.5, 3.5),
];
let ring = intersect_polygon(&a, &b, Manifold::Planar).expect("padded planar clip");
assert!(
(shoelace_area(&ring) - 1.0).abs() < TOL,
"area {}",
shoelace_area(&ring)
);
}
#[test]
fn planar_bbox_reject_is_strict_and_precedes_coercion() {
let degenerate = [(10.0, 10.0), (11.0, 11.0)];
let square = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
let ring = intersect_polygon(°enerate, &square, Manifold::Planar).expect("rejected");
assert!(ring.is_empty());
assert!(
intersect_polygon(&[], &square, Manifold::Planar)
.unwrap()
.is_empty()
);
let touching = [(1.0, 0.0), (1.0, 1.0)];
assert!(intersect_polygon(&touching, &square, Manifold::Planar).is_err());
assert!(intersect_polygon(°enerate, &square, Manifold::Spherical).is_err());
assert_eq!(
planar_ring_bbox(&[(f64::NAN, 0.0), (5.0, 1.0)]).map(|b| b[1].is_nan()),
Some(true)
);
assert!(!bboxes_disjoint(
&[f64::NAN, f64::NAN, 0.0, 1.0],
&[5.0, 6.0, 0.0, 1.0]
));
}
#[test]
fn as_ring_rejects_sub_three_distinct() {
let degenerate = [(0.0, 0.0), (1.0, 1.0), (1.0, 1.0), (0.0, 0.0)];
assert!(intersect_polygon(°enerate, °enerate, Manifold::Planar).is_err());
}
#[test]
fn spherical_clip_accepts_padded_rings() {
let a = [(0.0, 0.0), (90.0, 0.0), (0.0, 90.0), (0.0, 90.0)];
let b = [
(45.0, 0.0),
(135.0, 0.0),
(45.0, 90.0),
(45.0, 90.0),
(45.0, 90.0),
];
let ring = intersect_polygon(&a, &b, Manifold::Spherical)
.expect("padded spherical clip must succeed (S2 no longer sees the degenerate edge)");
assert!(
ring.len() >= 3,
"expected a non-empty spherical overlap ring"
);
let area = spherical_area(&ring).expect("spherical area");
assert!(
(area - std::f64::consts::FRAC_PI_4).abs() < 1e-9,
"padded spherical overlap area was {area}, expected π/4"
);
let a_u = [(0.0, 0.0), (90.0, 0.0), (0.0, 90.0)];
let b_u = [(45.0, 0.0), (135.0, 0.0), (45.0, 90.0)];
let ring_u = intersect_polygon(&a_u, &b_u, Manifold::Spherical).expect("unpadded");
assert!((spherical_area(&ring).unwrap() - spherical_area(&ring_u).unwrap()).abs() < 1e-12);
}
#[test]
fn geodesic_manifold_uses_the_spherical_path() {
let a = [(0.0, 0.0), (90.0, 0.0), (0.0, 90.0)];
let b = [(45.0, 0.0), (135.0, 0.0), (45.0, 90.0)];
let sph = intersect_polygon(&a, &b, Manifold::Spherical).expect("spherical");
let geo = intersect_polygon(&a, &b, Manifold::Geodesic).expect("geodesic");
assert_eq!(
sph, geo,
"geodesic clips along great circles, same as spherical"
);
}
#[test]
fn polygon_area_dispatches_on_manifold() {
let ring = [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0)];
assert!((polygon_area(&ring, Manifold::Planar).unwrap() - 1.0).abs() < TOL);
{
let octant = [(0.0, 0.0), (90.0, 0.0), (0.0, 90.0)];
let area = polygon_area(&octant, Manifold::Spherical).unwrap();
assert!(
(area - std::f64::consts::FRAC_PI_2).abs() < 1e-9,
"area {area}"
);
}
}
#[test]
fn tolerance_exact_match_passes() {
assert!(area_tolerance_ok(1.0, 1.0, 1e-12, 1.0));
}
#[test]
fn tolerance_sub_atol_slivers_are_zero() {
assert!(area_tolerance_ok(1e-20, 0.0, 1e-12, 1.0));
assert!(area_tolerance_ok(0.0, 1e-20, 1e-12, 1.0));
}
#[test]
fn tolerance_gross_disagreement_fails() {
assert!(!area_tolerance_ok(1.0, 2.0, 1e-9, 1.0));
}
#[test]
fn tolerance_relative_band_scales_with_reference() {
assert!(area_tolerance_ok(1.0 + 1e-6, 1.0, 1e-5, 1.0));
assert!(!area_tolerance_ok(1.0 + 1e-6, 1.0, 1e-7, 1.0));
}
#[test]
fn sliver_floor_scales_with_radius_squared() {
assert_eq!(sliver_atol(1.0), 1e-15);
assert!((sliver_atol(1000.0) - 1e-15 * 1e6).abs() < 1e-30);
}
fn lonlat_to_unit(lon_deg: f64, lat_deg: f64) -> (f64, f64, f64) {
let lon = lon_deg.to_radians();
let lat = lat_deg.to_radians();
let cos_lat = lat.cos();
(cos_lat * lon.cos(), cos_lat * lon.sin(), lat.sin())
}
fn triangle_excess(a: (f64, f64, f64), b: (f64, f64, f64), c: (f64, f64, f64)) -> f64 {
let cross = (
b.1 * c.2 - b.2 * c.1,
b.2 * c.0 - b.0 * c.2,
b.0 * c.1 - b.1 * c.0,
);
let triple = a.0 * cross.0 + a.1 * cross.1 + a.2 * cross.2;
let dot_ab = a.0 * b.0 + a.1 * b.1 + a.2 * b.2;
let dot_bc = b.0 * c.0 + b.1 * c.1 + b.2 * c.2;
let dot_ca = c.0 * a.0 + c.1 * a.1 + c.2 * a.2;
2.0 * triple.atan2(1.0 + dot_ab + dot_bc + dot_ca)
}
fn spherical_excess_area(ring: &[(f64, f64)]) -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let v: Vec<(f64, f64, f64)> = ring
.iter()
.map(|&(lo, la)| lonlat_to_unit(lo, la))
.collect();
let mut total = 0.0;
for i in 1..n - 1 {
total += triangle_excess(v[0], v[i], v[i + 1]);
}
total.abs()
}
fn true_cell_area(lon1: f64, lon2: f64, lat1: f64, lat2: f64) -> f64 {
(lon2 - lon1).to_radians() * (lat2.to_radians().sin() - lat1.to_radians().sin())
}
#[test]
fn densification_reduces_coarse_polar_cell_area_error() {
let cell = [(0.0, 60.0), (30.0, 60.0), (30.0, 80.0), (0.0, 80.0)];
let a_true = true_cell_area(0.0, 30.0, 60.0, 80.0);
let a_coarse = spherical_excess_area(&cell);
let err_coarse = (a_coarse - a_true).abs() / a_true;
assert!(err_coarse > 0.02, "err_coarse was {err_coarse}");
let dense = densify_parallel_edges(&cell, 1.0, DEFAULT_LAT_ATOL).expect("densify");
assert!(dense.len() > cell.len(), "vertices were inserted");
let a_dense = spherical_excess_area(&dense);
let err_dense = (a_dense - a_true).abs() / a_true;
assert!(err_dense < err_coarse, "densification reduces the error");
assert!(
err_dense < 1e-3,
"converges to the true area: err_dense {err_dense}"
);
let dense5 = densify_parallel_edges(&cell, 5.0, DEFAULT_LAT_ATOL).expect("densify");
let err_5 = (spherical_excess_area(&dense5) - a_true).abs() / a_true;
assert!(
err_dense < err_5 && err_5 < err_coarse,
"monotone densification: {err_dense} < {err_5} < {err_coarse}"
);
}
#[test]
fn densification_only_touches_parallel_edges_and_is_opt_in() {
let quad = [(0.0, 0.0), (0.0, 10.0), (1.0, 10.0), (1.0, 0.0)];
let dense = densify_parallel_edges(&quad, 0.5, DEFAULT_LAT_ATOL).expect("densify");
assert_eq!(dense.len(), 4 + 2);
assert_eq!(
densify_parallel_edges(&quad, 5.0, DEFAULT_LAT_ATOL)
.unwrap()
.len(),
4
);
assert!(densify_parallel_edges(&quad, 0.0, DEFAULT_LAT_ATOL).is_err());
}
#[test]
fn densified_vertices_stay_on_the_parallel() {
let cell = [(0.0, 70.0), (40.0, 70.0), (40.0, 71.0), (0.0, 71.0)];
let dense = densify_parallel_edges(&cell, 10.0, DEFAULT_LAT_ATOL).expect("densify");
for &(_, lat) in &dense {
assert!(
(lat - 70.0).abs() < 1e-9 || (lat - 71.0).abs() < 1e-9,
"vertex latitude {lat} is not on either parallel"
);
}
}
#[test]
fn densify_strips_closing_duplicate_and_rejects_degenerate() {
let closed = [
(0.0, 50.0),
(20.0, 50.0),
(20.0, 51.0),
(0.0, 51.0),
(0.0, 50.0),
];
let dense = densify_parallel_edges(&closed, 5.0, DEFAULT_LAT_ATOL).expect("densify");
assert_eq!(dense.len(), 4 + 3 + 3);
let degenerate = [(0.0, 0.0), (1.0, 0.0)];
assert!(densify_parallel_edges(°enerate, 1.0, DEFAULT_LAT_ATOL).is_err());
}
}