use std::collections::HashMap;
use ndarray::{ArrayD, IxDyn};
use serde_json::{Value as Json, json};
use crate::geometry::Manifold;
use crate::simulate_array::{Value, eval_expression};
use crate::types::Expr;
fn col(idx: &Json, c: i64) -> Json {
json!({ "op": "index", "args": ["overlap_clip", idx, c] })
}
fn unit_vec(idx: &Json) -> [Json; 3] {
let d = std::f64::consts::PI / 180.0;
let lon = json!({ "op": "*", "args": [col(idx, 1), d] });
let lat = json!({ "op": "*", "args": [col(idx, 2), d] });
let cos_lat = json!({ "op": "cos", "args": [lat.clone()] });
[
json!({ "op": "*", "args": [cos_lat.clone(), { "op": "cos", "args": [lon.clone()] }] }),
json!({ "op": "*", "args": [cos_lat, { "op": "sin", "args": [lon] }] }),
json!({ "op": "sin", "args": [lat] }),
]
}
fn dot3(u: &[Json; 3], v: &[Json; 3]) -> Json {
json!({ "op": "+", "args": [
{ "op": "*", "args": [u[0].clone(), v[0].clone()] },
{ "op": "*", "args": [u[1].clone(), v[1].clone()] },
{ "op": "*", "args": [u[2].clone(), v[2].clone()] },
] })
}
fn cross3(u: &[Json; 3], v: &[Json; 3]) -> [Json; 3] {
[
json!({ "op": "-", "args": [{ "op": "*", "args": [u[1].clone(), v[2].clone()] }, { "op": "*", "args": [u[2].clone(), v[1].clone()] }] }),
json!({ "op": "-", "args": [{ "op": "*", "args": [u[2].clone(), v[0].clone()] }, { "op": "*", "args": [u[0].clone(), v[2].clone()] }] }),
json!({ "op": "-", "args": [{ "op": "*", "args": [u[0].clone(), v[1].clone()] }, { "op": "*", "args": [u[1].clone(), v[0].clone()] }] }),
]
}
fn spherical_excess(a: &[Json; 3], b: &[Json; 3], c: &[Json; 3]) -> Json {
let triple = dot3(a, &cross3(b, c));
let denom = json!({ "op": "+", "args": [1.0, dot3(a, b), dot3(b, c), dot3(c, a)] });
json!({ "op": "*", "args": [2.0, { "op": "atan2", "args": [triple, denom] }] })
}
pub(crate) fn shoelace_faq_node(n: usize) -> Expr {
let v: Json = json!("v");
let v_next: Json = json!({ "op": "+", "args": ["v", 1] });
serde_json::from_value(json!({
"op": "faq", "args": [], "semiring": "sum_product", "output_idx": [],
"ranges": { "v": [1, n] },
"expr": { "op": "*", "args": [0.5, { "op": "-", "args": [
{ "op": "*", "args": [col(&v, 1), col(&v_next, 2)] },
{ "op": "*", "args": [col(&v_next, 1), col(&v, 2)] },
] }] },
}))
.expect("shoelace area FAQ node is well-formed")
}
pub(crate) fn spherical_excess_faq_node(n: usize) -> Expr {
let apex = unit_vec(&json!(1));
let here = unit_vec(&json!("v"));
let next = unit_vec(&json!({ "op": "+", "args": ["v", 1] }));
serde_json::from_value(json!({
"op": "faq", "args": [], "semiring": "sum_product", "output_idx": [],
"ranges": { "v": [1, n] },
"expr": spherical_excess(&apex, &here, &next),
}))
.expect("spherical area FAQ node is well-formed")
}
pub fn polygon_area_faq(ring: &[(f64, f64)], manifold: Manifold) -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let mut flat: Vec<f64> = Vec::with_capacity((n + 1) * 2);
for &(x, y) in ring {
flat.push(x);
flat.push(y);
}
flat.push(ring[0].0);
flat.push(ring[0].1);
let clip = ArrayD::from_shape_vec(IxDyn(&[n + 1, 2]), flat).expect("closed ring is [n+1, 2]");
let faq = match manifold {
Manifold::Planar => shoelace_faq_node(n),
Manifold::Spherical | Manifold::Geodesic => spherical_excess_faq_node(n),
};
let mut inputs: HashMap<String, ArrayD<f64>> = HashMap::new();
inputs.insert("overlap_clip".to_string(), clip);
let value = eval_expression(&faq, &inputs, &[], &[], 0.0)
.expect("the polygon-area FAQ node is built from evaluable ops by construction");
match value {
Value::Scalar(s) => s.abs(),
Value::Array(_) => unreachable!("scalar polygon_area FAQ must reduce to a scalar"),
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use crate::geometry;
use std::f64::consts::PI;
const TIGHT: f64 = 1e-12;
#[test]
fn planar_area_faq_matches_shoelace_oracle() {
let ring = [(0.0, 0.0), (4.0, 0.0), (4.0, 3.0), (1.0, 5.0), (0.0, 3.0)];
let faq = polygon_area_faq(&ring, Manifold::Planar);
assert!((faq - geometry::shoelace_area(&ring)).abs() < TIGHT);
}
#[test]
fn spherical_area_faq_octant_is_pi_over_two() {
let octant = [(0.0, 0.0), (90.0, 0.0), (0.0, 90.0)];
let faq = polygon_area_faq(&octant, Manifold::Spherical);
assert!((faq - PI / 2.0).abs() < TIGHT, "octant FAQ {faq} vs π/2");
}
#[test]
fn spherical_area_faq_matches_s2_oracle() {
let ring = [(10.0, 20.0), (30.0, 22.0), (28.0, 40.0), (8.0, 38.0)];
for manifold in [Manifold::Spherical, Manifold::Geodesic] {
let faq = polygon_area_faq(&ring, manifold);
let oracle = geometry::polygon_area(&ring, manifold).unwrap();
assert!(
geometry::area_tolerance_ok(faq, oracle, 1e-9, 1.0),
"{manifold:?} FAQ {faq} vs s2 oracle {oracle}",
);
}
}
#[test]
fn degenerate_ring_is_zero_area() {
let two = [(0.0, 0.0), (1.0, 1.0)];
assert_eq!(polygon_area_faq(&two, Manifold::Planar), 0.0);
assert_eq!(polygon_area_faq(&two, Manifold::Spherical), 0.0);
}
}