use crate::astro::math::special::erf;
use crate::dop::{self, PositionCovariance};
use crate::error_metrics::{self, PercentileRadius};
use crate::frame::Wgs84Geodetic;
use crate::geodesic::{geodesic_direct, geodesic_inverse, GeodesicError};
const DEG_TO_RAD: f64 = core::f64::consts::PI / 180.0;
const RAD_TO_DEG: f64 = 180.0 / core::f64::consts::PI;
const TWO_PI: f64 = 2.0 * core::f64::consts::PI;
const EDGE_MINIMIZE_ITERS: usize = 64;
const PSD_REL_TOL: f64 = 1.0e-12;
const RAY_EPS: f64 = 1.0e-12;
const RAY_PROBE: f64 = 1.0e-7;
const DEDUP_EPS: f64 = 1.0e-9;
const ORIENTATION_PROBE_MAX_M: f64 = 1.0;
const ORIENTATION_PROBE_FRACTION: f64 = 1.0e-3;
const ORIENTATION_CENTROID_NORM_TOL: f64 = 1.0e-12;
pub const PLANAR_FAST_PATH_MAX_RADIUS_M: f64 = 50_000.0;
pub const GEOFENCE_BOUNDARY_TOLERANCE_M: f64 = 1.0e-4;
#[allow(clippy::excessive_precision)]
const GL64_POSITIVE_NODES: [f64; 32] = [
2.43502926634244325e-02,
7.29931217877990424e-02,
1.21462819296120544e-01,
1.69644420423992831e-01,
2.17423643740007083e-01,
2.64687162208767424e-01,
3.11322871990210970e-01,
3.57220158337668126e-01,
4.02270157963991570e-01,
4.46366017253464087e-01,
4.89403145707052956e-01,
5.31279464019894565e-01,
5.71895646202634000e-01,
6.11155355172393278e-01,
6.48965471254657311e-01,
6.85236313054233270e-01,
7.19881850171610771e-01,
7.52819907260531940e-01,
7.83972358943341385e-01,
8.13265315122797539e-01,
8.40629296252580316e-01,
8.65999398154092770e-01,
8.89315445995114140e-01,
9.10522137078502825e-01,
9.29569172131939570e-01,
9.46411374858402765e-01,
9.61008799652053658e-01,
9.73326827789910975e-01,
9.83336253884625977e-01,
9.91013371476744287e-01,
9.96340116771955220e-01,
9.99305041735772170e-01,
];
#[allow(clippy::excessive_precision)]
const GL64_POSITIVE_WEIGHTS: [f64; 32] = [
4.86909570091397237e-02,
4.85754674415033935e-02,
4.83447622348029404e-02,
4.79993885964583034e-02,
4.75401657148303222e-02,
4.69681828162099788e-02,
4.62847965813143539e-02,
4.54916279274180727e-02,
4.45905581637566079e-02,
4.35837245293234157e-02,
4.24735151236535352e-02,
4.12625632426234581e-02,
3.99537411327204536e-02,
3.85501531786155635e-02,
3.70551285402399844e-02,
3.54722132568822401e-02,
3.38051618371418630e-02,
3.20579283548514254e-02,
3.02346570724024884e-02,
2.83396726142594486e-02,
2.63774697150548978e-02,
2.43527025687111306e-02,
2.22701738083829967e-02,
2.01348231535300216e-02,
1.79517157756973571e-02,
1.57260304760250269e-02,
1.34630478967191179e-02,
1.11681394601309738e-02,
8.84675982636339009e-03,
6.50445796897848854e-03,
4.14703326056443250e-03,
1.78328072169469699e-03,
];
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
pub enum GeofenceError {
#[error("geofence needs at least three vertices")]
TooFewVertices,
#[error("invalid geofence input {field}: {reason}")]
InvalidInput {
field: &'static str,
reason: &'static str,
},
#[error(transparent)]
Geodesic(#[from] GeodesicError),
#[error("covariance rotation failed")]
Dop(#[from] dop::DopError),
#[error("uncertainty validation failed")]
ErrorMetrics(error_metrics::ErrorMetricsError),
}
#[derive(Debug, Clone, PartialEq)]
pub struct Fence {
vertices: Vec<Wgs84Geodetic>,
edges: Vec<GeodesicEdge>,
interior_winding_sign: f64,
planar: Option<PlanarCache>,
}
impl Fence {
pub fn new<I>(vertices: I) -> Result<Self, GeofenceError>
where
I: IntoIterator<Item = Wgs84Geodetic>,
{
let mut vertices: Vec<Wgs84Geodetic> = vertices.into_iter().collect();
if vertices.len() >= 2
&& same_horizontal_position(vertices[0], vertices[vertices.len() - 1])?
{
vertices.pop();
}
if vertices.len() < 3 {
return Err(GeofenceError::TooFewVertices);
}
validate_distinct_vertices(&vertices)?;
let mut edges = Vec::with_capacity(vertices.len());
for idx in 0..vertices.len() {
let start = vertices[idx];
let end = vertices[(idx + 1) % vertices.len()];
let (length_m, azimuth_deg, _) = inverse_points(start, end)?;
if length_m == 0.0 {
return Err(invalid_input("vertices", "adjacent vertices must differ"));
}
edges.push(GeodesicEdge {
start,
length_m,
azimuth_deg,
});
}
let interior_winding_sign = interior_winding_sign(&vertices, &edges)?;
let planar = build_planar_cache(&vertices, &edges)?;
Ok(Self {
vertices,
edges,
interior_winding_sign,
planar,
})
}
pub fn vertices(&self) -> &[Wgs84Geodetic] {
&self.vertices
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
pub fn planar_fast_path_applies(&self, position: Wgs84Geodetic) -> bool {
self.planar
.as_ref()
.and_then(|cache| project_from(cache.anchor, position).ok())
.is_some_and(|projected| norm2(projected) <= PLANAR_FAST_PATH_MAX_RADIUS_M)
}
pub fn contains(&self, position: Wgs84Geodetic) -> Result<bool, GeofenceError> {
Ok(self.distance_to_boundary(position)? >= -GEOFENCE_BOUNDARY_TOLERANCE_M)
}
pub fn distance_to_boundary(&self, position: Wgs84Geodetic) -> Result<f64, GeofenceError> {
Ok(self.boundary_distance_geodesic(position)?.signed_distance_m)
}
pub fn distance_to_boundary_planar_fast(
&self,
position: Wgs84Geodetic,
) -> Result<Option<f64>, GeofenceError> {
if let Some(cache) = &self.planar {
if let Ok(point) = project_from(cache.anchor, position) {
if norm2(point) <= PLANAR_FAST_PATH_MAX_RADIUS_M {
return Ok(Some(
boundary_distance_planar(&cache.vertices_m, point).signed_distance_m,
));
}
}
}
Ok(None)
}
fn boundary_distance(
&self,
position: Wgs84Geodetic,
) -> Result<BoundaryDistance, GeofenceError> {
self.boundary_distance_geodesic(position)
}
fn boundary_distance_geodesic(
&self,
position: Wgs84Geodetic,
) -> Result<BoundaryDistance, GeofenceError> {
let mut nearest = None;
for (edge_index, edge) in self.edges.iter().enumerate() {
let candidate = nearest_on_edge(position, *edge, edge_index)?;
if nearest
.as_ref()
.is_none_or(|closest: &NearestBoundary| candidate.distance_m < closest.distance_m)
{
nearest = Some(candidate);
}
}
let nearest = nearest.ok_or(GeofenceError::TooFewVertices)?;
if nearest.distance_m <= GEOFENCE_BOUNDARY_TOLERANCE_M {
return Ok(BoundaryDistance {
signed_distance_m: 0.0,
normal_en: tangent_normal(self.edges[nearest.edge_index].azimuth_deg),
});
}
let inside = contains_by_winding(position, &self.vertices, self.interior_winding_sign)?;
let sign = if inside { 1.0 } else { -1.0 };
let normal_en =
normal_from_position_to_boundary(position, nearest.point, nearest.distance_m)
.unwrap_or_else(|_| tangent_normal(self.edges[nearest.edge_index].azimuth_deg));
Ok(BoundaryDistance {
signed_distance_m: sign * nearest.distance_m,
normal_en,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PositionUncertainty {
EnuCovarianceM2([[f64; 3]; 3]),
EcefCovarianceM2([[f64; 3]; 3]),
PositionCovariance(PositionCovariance),
HorizontalRadius(PercentileRadius),
CepRadiusM(f64),
}
impl From<PositionCovariance> for PositionUncertainty {
fn from(value: PositionCovariance) -> Self {
Self::PositionCovariance(value)
}
}
impl From<PercentileRadius> for PositionUncertainty {
fn from(value: PercentileRadius) -> Self {
Self::HorizontalRadius(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProbabilityMethod {
BoundaryNormal,
PlanarQuadrature,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProbabilityOptions {
pub method: ProbabilityMethod,
}
impl Default for ProbabilityOptions {
fn default() -> Self {
Self {
method: ProbabilityMethod::BoundaryNormal,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GeofencePositionEstimate {
pub position: Wgs84Geodetic,
pub uncertainty: PositionUncertainty,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ProbabilityHysteresis {
pub enter_confidence: f64,
pub leave_confidence: f64,
}
impl ProbabilityHysteresis {
pub fn new(enter_confidence: f64, leave_confidence: f64) -> Result<Self, GeofenceError> {
validate_confidence("enter_confidence", enter_confidence)?;
validate_confidence("leave_confidence", leave_confidence)?;
Ok(Self {
enter_confidence,
leave_confidence,
})
}
}
impl Default for ProbabilityHysteresis {
fn default() -> Self {
Self {
enter_confidence: 0.95,
leave_confidence: 0.95,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrossingKind {
Entered,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CrossingEvent {
pub sample_index: usize,
pub kind: CrossingKind,
pub inside_probability: f64,
}
pub fn containment(position: Wgs84Geodetic, fence: &Fence) -> Result<bool, GeofenceError> {
fence.contains(position)
}
pub fn distance_to_boundary(position: Wgs84Geodetic, fence: &Fence) -> Result<f64, GeofenceError> {
fence.distance_to_boundary(position)
}
pub fn containment_probability(
position: Wgs84Geodetic,
uncertainty: PositionUncertainty,
fence: &Fence,
) -> Result<f64, GeofenceError> {
containment_probability_with_options(
position,
uncertainty,
fence,
ProbabilityOptions::default(),
)
}
pub fn containment_probability_with_options(
position: Wgs84Geodetic,
uncertainty: PositionUncertainty,
fence: &Fence,
options: ProbabilityOptions,
) -> Result<f64, GeofenceError> {
let boundary = fence.boundary_distance(position)?;
let covariance = uncertainty_to_enu_m2(uncertainty, position)?;
match options.method {
ProbabilityMethod::BoundaryNormal => {
probability_boundary_normal_or_quadrature(fence, position, boundary, covariance)
}
ProbabilityMethod::PlanarQuadrature => {
probability_planar_quadrature(fence, position, boundary, covariance)
}
}
}
pub fn crossing(
positions: &[Wgs84Geodetic],
fence: &Fence,
) -> Result<Vec<CrossingEvent>, GeofenceError> {
let mut events = Vec::new();
let Some((&first, rest)) = positions.split_first() else {
return Ok(events);
};
let mut inside = fence.contains(first)?;
for (offset, &position) in rest.iter().enumerate() {
let next_inside = fence.contains(position)?;
if next_inside != inside {
events.push(CrossingEvent {
sample_index: offset + 1,
kind: if next_inside {
CrossingKind::Entered
} else {
CrossingKind::Left
},
inside_probability: if next_inside { 1.0 } else { 0.0 },
});
inside = next_inside;
}
}
Ok(events)
}
pub fn crossing_probability(
samples: &[GeofencePositionEstimate],
fence: &Fence,
hysteresis: ProbabilityHysteresis,
) -> Result<Vec<CrossingEvent>, GeofenceError> {
crossing_probability_with_options(samples, fence, hysteresis, ProbabilityOptions::default())
}
pub fn crossing_probability_with_options(
samples: &[GeofencePositionEstimate],
fence: &Fence,
hysteresis: ProbabilityHysteresis,
options: ProbabilityOptions,
) -> Result<Vec<CrossingEvent>, GeofenceError> {
validate_confidence("enter_confidence", hysteresis.enter_confidence)?;
validate_confidence("leave_confidence", hysteresis.leave_confidence)?;
let mut events = Vec::new();
let Some((first, rest)) = samples.split_first() else {
return Ok(events);
};
let first_probability =
containment_probability_with_options(first.position, first.uncertainty, fence, options)?;
let outside_threshold = 1.0 - hysteresis.leave_confidence;
let mut state = HysteresisState::from_probability(
first_probability,
hysteresis.enter_confidence,
outside_threshold,
);
for (offset, sample) in rest.iter().enumerate() {
let probability = containment_probability_with_options(
sample.position,
sample.uncertainty,
fence,
options,
)?;
match state {
HysteresisState::Unknown => {
state = HysteresisState::from_probability(
probability,
hysteresis.enter_confidence,
outside_threshold,
);
}
HysteresisState::Outside if probability >= hysteresis.enter_confidence => {
state = HysteresisState::Inside;
events.push(CrossingEvent {
sample_index: offset + 1,
kind: CrossingKind::Entered,
inside_probability: probability,
});
}
HysteresisState::Inside if probability <= outside_threshold => {
state = HysteresisState::Outside;
events.push(CrossingEvent {
sample_index: offset + 1,
kind: CrossingKind::Left,
inside_probability: probability,
});
}
_ => {}
}
}
Ok(events)
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct GeodesicEdge {
start: Wgs84Geodetic,
length_m: f64,
azimuth_deg: f64,
}
#[derive(Debug, Clone, PartialEq)]
struct PlanarCache {
anchor: Wgs84Geodetic,
vertices_m: Vec<[f64; 2]>,
}
#[derive(Debug, Clone, Copy)]
struct BoundaryDistance {
signed_distance_m: f64,
normal_en: [f64; 2],
}
#[derive(Debug, Clone, Copy)]
struct NearestBoundary {
distance_m: f64,
point: Wgs84Geodetic,
edge_index: usize,
}
#[derive(Debug, Clone, Copy)]
struct HorizontalEigen {
major_m2: f64,
minor_m2: f64,
major_axis: [f64; 2],
minor_axis: [f64; 2],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HysteresisState {
Unknown,
Inside,
Outside,
}
impl HysteresisState {
fn from_probability(probability: f64, enter_confidence: f64, outside_threshold: f64) -> Self {
if probability >= enter_confidence {
Self::Inside
} else if probability <= outside_threshold {
Self::Outside
} else {
Self::Unknown
}
}
}
fn invalid_input(field: &'static str, reason: &'static str) -> GeofenceError {
GeofenceError::InvalidInput { field, reason }
}
fn validate_probability(field: &'static str, probability: f64) -> Result<(), GeofenceError> {
if probability.is_finite() && probability > 0.0 && probability < 1.0 {
Ok(())
} else {
Err(invalid_input(field, "must be in (0, 1)"))
}
}
fn validate_confidence(field: &'static str, probability: f64) -> Result<(), GeofenceError> {
if probability.is_finite() && probability > 0.5 && probability < 1.0 {
Ok(())
} else {
Err(invalid_input(field, "must be in (0.5, 1)"))
}
}
fn same_horizontal_position(a: Wgs84Geodetic, b: Wgs84Geodetic) -> Result<bool, GeofenceError> {
if a.lat_rad.to_bits() == b.lat_rad.to_bits() && a.lon_rad.to_bits() == b.lon_rad.to_bits() {
return Ok(true);
}
let (distance_m, _, _) = inverse_points(a, b)?;
Ok(distance_m <= GEOFENCE_BOUNDARY_TOLERANCE_M)
}
fn validate_distinct_vertices(vertices: &[Wgs84Geodetic]) -> Result<(), GeofenceError> {
let mut distinct_count = 0;
'vertex: for (idx, &vertex) in vertices.iter().enumerate() {
for &previous in &vertices[..idx] {
if same_horizontal_position(vertex, previous)? {
continue 'vertex;
}
}
distinct_count += 1;
}
if distinct_count < 3 {
return Err(GeofenceError::TooFewVertices);
}
if distinct_count != vertices.len() {
return Err(invalid_input("vertices", "vertices must be distinct"));
}
Ok(())
}
fn interior_winding_sign(
vertices: &[Wgs84Geodetic],
edges: &[GeodesicEdge],
) -> Result<f64, GeofenceError> {
let mut xyz = [0.0_f64; 3];
for &vertex in vertices {
let cos_lat = vertex.lat_rad.cos();
xyz[0] += cos_lat * vertex.lon_rad.cos();
xyz[1] += cos_lat * vertex.lon_rad.sin();
xyz[2] += vertex.lat_rad.sin();
}
let norm = (xyz[0] * xyz[0] + xyz[1] * xyz[1] + xyz[2] * xyz[2]).sqrt();
if norm > ORIENTATION_CENTROID_NORM_TOL {
let lat = (xyz[2] / norm).asin();
let lon = xyz[1].atan2(xyz[0]);
let probe = Wgs84Geodetic::new(lat, lon, 0.0)
.map_err(|_| invalid_input("vertices", "centroid is outside geodetic range"))?;
let sum = winding_sum(probe, vertices)?;
if sum.abs() > core::f64::consts::PI {
return Ok(sum.signum());
}
}
for edge in edges {
if let Some(sign) = edge_orientation_probe(vertices, *edge)? {
return Ok(sign);
}
}
Err(invalid_input(
"vertices",
"interior orientation is ambiguous",
))
}
fn edge_orientation_probe(
vertices: &[Wgs84Geodetic],
edge: GeodesicEdge,
) -> Result<Option<f64>, GeofenceError> {
let midpoint = direct_point(edge.start, edge.azimuth_deg, 0.5 * edge.length_m)?;
let offset_m = (edge.length_m * ORIENTATION_PROBE_FRACTION).clamp(
GEOFENCE_BOUNDARY_TOLERANCE_M * 10.0,
ORIENTATION_PROBE_MAX_M,
);
let left = direct_point(midpoint, edge.azimuth_deg - 90.0, offset_m)?;
let right = direct_point(midpoint, edge.azimuth_deg + 90.0, offset_m)?;
let left_sum = winding_sum(left, vertices)?;
let right_sum = winding_sum(right, vertices)?;
let left_inside = left_sum.abs() > core::f64::consts::PI;
let right_inside = right_sum.abs() > core::f64::consts::PI;
match (left_inside, right_inside) {
(true, false) => Ok(Some(left_sum.signum())),
(false, true) => Ok(Some(right_sum.signum())),
(true, true) => Err(invalid_input(
"vertices",
"interior orientation is ambiguous",
)),
(false, false) => Ok(None),
}
}
fn inverse_points(a: Wgs84Geodetic, b: Wgs84Geodetic) -> Result<(f64, f64, f64), GeofenceError> {
Ok(geodesic_inverse(
a.lat_rad * RAD_TO_DEG,
a.lon_rad * RAD_TO_DEG,
b.lat_rad * RAD_TO_DEG,
b.lon_rad * RAD_TO_DEG,
)?)
}
fn direct_point(
start: Wgs84Geodetic,
azimuth_deg: f64,
distance_m: f64,
) -> Result<Wgs84Geodetic, GeofenceError> {
let (lat_deg, lon_deg, _) = geodesic_direct(
start.lat_rad * RAD_TO_DEG,
start.lon_rad * RAD_TO_DEG,
azimuth_deg,
distance_m,
)?;
Wgs84Geodetic::new(lat_deg * DEG_TO_RAD, lon_deg * DEG_TO_RAD, 0.0)
.map_err(|_| invalid_input("geodesic_direct", "returned invalid geodetic position"))
}
fn build_planar_cache(
vertices: &[Wgs84Geodetic],
edges: &[GeodesicEdge],
) -> Result<Option<PlanarCache>, GeofenceError> {
if edges
.iter()
.any(|edge| edge.length_m > PLANAR_FAST_PATH_MAX_RADIUS_M)
{
return Ok(None);
}
let anchor = vertices[0];
let mut vertices_m = Vec::with_capacity(vertices.len());
for &vertex in vertices {
let projected = project_from(anchor, vertex)?;
if norm2(projected) > PLANAR_FAST_PATH_MAX_RADIUS_M {
return Ok(None);
}
vertices_m.push(projected);
}
Ok(Some(PlanarCache { anchor, vertices_m }))
}
fn project_from(origin: Wgs84Geodetic, point: Wgs84Geodetic) -> Result<[f64; 2], GeofenceError> {
let (distance_m, azimuth_deg, _) = inverse_points(origin, point)?;
if distance_m == 0.0 {
return Ok([0.0, 0.0]);
}
let azimuth_rad = azimuth_deg * DEG_TO_RAD;
Ok([
distance_m * azimuth_rad.sin(),
distance_m * azimuth_rad.cos(),
])
}
fn boundary_distance_planar(vertices_m: &[[f64; 2]], point: [f64; 2]) -> BoundaryDistance {
let mut closest_distance = f64::INFINITY;
let mut closest_normal = [1.0, 0.0];
for idx in 0..vertices_m.len() {
let a = vertices_m[idx];
let b = vertices_m[(idx + 1) % vertices_m.len()];
let (distance, normal) = distance_to_segment_planar(point, a, b);
if distance < closest_distance {
closest_distance = distance;
closest_normal = normal;
}
}
if closest_distance <= GEOFENCE_BOUNDARY_TOLERANCE_M {
return BoundaryDistance {
signed_distance_m: 0.0,
normal_en: closest_normal,
};
}
let inside = point_in_polygon_planar(vertices_m, point);
BoundaryDistance {
signed_distance_m: if inside {
closest_distance
} else {
-closest_distance
},
normal_en: closest_normal,
}
}
fn distance_to_segment_planar(point: [f64; 2], a: [f64; 2], b: [f64; 2]) -> (f64, [f64; 2]) {
let ab = sub2(b, a);
let ap = sub2(point, a);
let denom = dot2(ab, ab);
let t = if denom == 0.0 {
0.0
} else {
(dot2(ap, ab) / denom).clamp(0.0, 1.0)
};
let nearest = [a[0] + t * ab[0], a[1] + t * ab[1]];
let delta = sub2(point, nearest);
let distance = norm2(delta);
if distance > 0.0 {
(distance, [delta[0] / distance, delta[1] / distance])
} else {
let tangent_norm = norm2(ab);
if tangent_norm == 0.0 {
(0.0, [1.0, 0.0])
} else {
(0.0, [-ab[1] / tangent_norm, ab[0] / tangent_norm])
}
}
}
fn nearest_on_edge(
position: Wgs84Geodetic,
edge: GeodesicEdge,
edge_index: usize,
) -> Result<NearestBoundary, GeofenceError> {
let mut closest = nearest_at(edge, position, 0.0, edge_index)?;
let end = nearest_at(edge, position, edge.length_m, edge_index)?;
if end.distance_m < closest.distance_m {
closest = end;
}
let mut lo = 0.0;
let mut hi = edge.length_m;
for _ in 0..EDGE_MINIMIZE_ITERS {
let third = (hi - lo) / 3.0;
let left = lo + third;
let right = hi - third;
let left_distance = distance_at(edge, position, left)?;
let right_distance = distance_at(edge, position, right)?;
if left_distance < right_distance {
hi = right;
} else {
lo = left;
}
}
let mid = 0.5 * (lo + hi);
let interior = nearest_at(edge, position, mid, edge_index)?;
if interior.distance_m < closest.distance_m {
closest = interior;
}
Ok(closest)
}
fn nearest_at(
edge: GeodesicEdge,
position: Wgs84Geodetic,
along_m: f64,
edge_index: usize,
) -> Result<NearestBoundary, GeofenceError> {
let point = direct_point(edge.start, edge.azimuth_deg, along_m)?;
let (distance_m, _, _) = inverse_points(position, point)?;
Ok(NearestBoundary {
distance_m,
point,
edge_index,
})
}
fn distance_at(
edge: GeodesicEdge,
position: Wgs84Geodetic,
along_m: f64,
) -> Result<f64, GeofenceError> {
let point = direct_point(edge.start, edge.azimuth_deg, along_m)?;
let (distance_m, _, _) = inverse_points(position, point)?;
Ok(distance_m)
}
fn normal_from_position_to_boundary(
position: Wgs84Geodetic,
boundary: Wgs84Geodetic,
distance_m: f64,
) -> Result<[f64; 2], GeofenceError> {
if distance_m == 0.0 {
return Ok([1.0, 0.0]);
}
let (_, azimuth_deg, _) = inverse_points(position, boundary)?;
let azimuth_rad = azimuth_deg * DEG_TO_RAD;
Ok([azimuth_rad.sin(), azimuth_rad.cos()])
}
fn tangent_normal(azimuth_deg: f64) -> [f64; 2] {
let azimuth_rad = azimuth_deg * DEG_TO_RAD;
let tangent = [azimuth_rad.sin(), azimuth_rad.cos()];
[-tangent[1], tangent[0]]
}
fn contains_by_winding(
position: Wgs84Geodetic,
vertices: &[Wgs84Geodetic],
interior_sign: f64,
) -> Result<bool, GeofenceError> {
let sum = winding_sum(position, vertices)?;
if sum.abs() <= core::f64::consts::PI {
return Ok(false);
}
if interior_sign != 0.0 {
Ok(sum.signum() == interior_sign)
} else {
Ok(false)
}
}
fn winding_sum(position: Wgs84Geodetic, vertices: &[Wgs84Geodetic]) -> Result<f64, GeofenceError> {
let mut sum = 0.0;
for idx in 0..vertices.len() {
let a = vertices[idx];
let b = vertices[(idx + 1) % vertices.len()];
let (distance_a, azimuth_a_deg, _) = inverse_points(position, a)?;
let (distance_b, azimuth_b_deg, _) = inverse_points(position, b)?;
if distance_a <= GEOFENCE_BOUNDARY_TOLERANCE_M
|| distance_b <= GEOFENCE_BOUNDARY_TOLERANCE_M
{
return Ok(TWO_PI);
}
sum += wrap_pi((azimuth_b_deg - azimuth_a_deg) * DEG_TO_RAD);
}
Ok(sum)
}
fn point_in_polygon_planar(vertices: &[[f64; 2]], point: [f64; 2]) -> bool {
let mut inside = false;
let mut prev = vertices[vertices.len() - 1];
for &curr in vertices {
let crosses = (curr[1] > point[1]) != (prev[1] > point[1]);
if crosses {
let x_at_y = curr[0] + (point[1] - curr[1]) * (prev[0] - curr[0]) / (prev[1] - curr[1]);
if point[0] < x_at_y {
inside = !inside;
}
}
prev = curr;
}
inside
}
fn uncertainty_to_enu_m2(
uncertainty: PositionUncertainty,
position: Wgs84Geodetic,
) -> Result<[[f64; 3]; 3], GeofenceError> {
let covariance = match uncertainty {
PositionUncertainty::EnuCovarianceM2(covariance) => covariance,
PositionUncertainty::EcefCovarianceM2(covariance) => {
dop::rotate_covariance_ecef_to_enu_m2(covariance, position)?
}
PositionUncertainty::PositionCovariance(covariance) => covariance.enu_m2,
PositionUncertainty::HorizontalRadius(radius) => covariance_from_horizontal_radius(radius)?,
PositionUncertainty::CepRadiusM(radius_m) => {
covariance_from_horizontal_radius(PercentileRadius {
probability: 0.5,
radius_m,
approx_m: radius_m,
approx_valid: true,
})?
}
};
error_metrics::metrics_from_enu_covariance_m2(covariance)
.map_err(GeofenceError::ErrorMetrics)?;
Ok(covariance)
}
fn covariance_from_horizontal_radius(
radius: PercentileRadius,
) -> Result<[[f64; 3]; 3], GeofenceError> {
validate_probability("radius.probability", radius.probability)?;
if !radius.radius_m.is_finite() || radius.radius_m < 0.0 {
return Err(invalid_input(
"radius.radius_m",
"must be finite non-negative",
));
}
let sigma2 = if radius.radius_m == 0.0 {
0.0
} else {
let scale = -2.0 * (1.0 - radius.probability).ln();
radius.radius_m * radius.radius_m / scale
};
Ok([[sigma2, 0.0, 0.0], [0.0, sigma2, 0.0], [0.0, 0.0, 0.0]])
}
fn probability_boundary_normal_or_quadrature(
fence: &Fence,
position: Wgs84Geodetic,
boundary: BoundaryDistance,
covariance: [[f64; 3]; 3],
) -> Result<f64, GeofenceError> {
let trace = covariance[0][0].max(0.0) + covariance[1][1].max(0.0);
let variance = projected_variance(covariance, boundary.normal_en).max(0.0);
if trace > 0.0 && variance <= trace * PSD_REL_TOL {
return probability_planar_quadrature(fence, position, boundary, covariance);
}
if variance == 0.0 {
if boundary.signed_distance_m == 0.0 {
return Ok(1.0);
}
return Ok(if boundary.signed_distance_m > 0.0 {
1.0
} else {
0.0
});
}
Ok(normal_cdf(boundary.signed_distance_m / variance.sqrt()).clamp(0.0, 1.0))
}
fn probability_planar_quadrature(
fence: &Fence,
position: Wgs84Geodetic,
boundary: BoundaryDistance,
covariance: [[f64; 3]; 3],
) -> Result<f64, GeofenceError> {
let vertices = local_vertices(fence, position)?;
let eigen = horizontal_eigen(covariance)?;
let scale = eigen.major_m2.abs().max(1.0);
if eigen.major_m2 <= scale * PSD_REL_TOL {
if boundary.signed_distance_m == 0.0 {
return Ok(1.0);
}
return Ok(if boundary.signed_distance_m > 0.0 {
1.0
} else {
0.0
});
}
let origin_inside = boundary.signed_distance_m > 0.0;
let origin_on_boundary = boundary.signed_distance_m == 0.0;
if eigen.minor_m2 <= eigen.major_m2 * PSD_REL_TOL {
return Ok(rank_one_probability(
&vertices,
origin_inside,
origin_on_boundary,
scale2(eigen.major_axis, eigen.major_m2.sqrt()),
));
}
let major_sigma = eigen.major_m2.sqrt();
let minor_sigma = eigen.minor_m2.sqrt();
let integral = integrate_gl64(0.0, TWO_PI, |theta| {
let unit = [theta.cos(), theta.sin()];
let direction = add2(
scale2(eigen.major_axis, major_sigma * unit[0]),
scale2(eigen.minor_axis, minor_sigma * unit[1]),
);
radial_mass_along_direction(&vertices, origin_inside, origin_on_boundary, direction)
});
Ok((integral / TWO_PI).clamp(0.0, 1.0))
}
fn local_vertices(fence: &Fence, position: Wgs84Geodetic) -> Result<Vec<[f64; 2]>, GeofenceError> {
fence
.vertices
.iter()
.map(|&vertex| project_from(position, vertex))
.collect()
}
fn projected_variance(covariance: [[f64; 3]; 3], normal_en: [f64; 2]) -> f64 {
let e = normal_en[0];
let n = normal_en[1];
e * e * covariance[0][0] + 2.0 * e * n * covariance[0][1] + n * n * covariance[1][1]
}
fn horizontal_eigen(covariance: [[f64; 3]; 3]) -> Result<HorizontalEigen, GeofenceError> {
let a = covariance[0][0];
let b = 0.5 * (covariance[0][1] + covariance[1][0]);
let c = covariance[1][1];
let center = 0.5 * (a + c);
let half_delta = 0.5 * (a - c);
let root = (half_delta * half_delta + b * b).sqrt();
let major = center + root;
let minor = center - root;
let scale = major.abs().max(minor.abs()).max(1.0);
if !major.is_finite() || !minor.is_finite() || minor < -scale * PSD_REL_TOL {
return Err(GeofenceError::ErrorMetrics(
error_metrics::ErrorMetricsError::NotPositiveSemidefinite,
));
}
let angle = if root == 0.0 {
0.0
} else {
0.5 * (2.0 * b).atan2(a - c)
};
let major_axis = [angle.cos(), angle.sin()];
let minor_axis = [-angle.sin(), angle.cos()];
Ok(HorizontalEigen {
major_m2: major.max(0.0),
minor_m2: minor.max(0.0),
major_axis,
minor_axis,
})
}
fn radial_mass_along_direction(
vertices: &[[f64; 2]],
origin_inside: bool,
origin_on_boundary: bool,
direction: [f64; 2],
) -> f64 {
if norm2(direction) == 0.0 {
return if origin_on_boundary || origin_inside {
1.0
} else {
0.0
};
}
let mut hits = ray_intersections(vertices, direction);
sort_dedup(&mut hits);
let mut inside = if origin_on_boundary {
point_in_polygon_planar(vertices, scale2(direction, RAY_PROBE))
} else {
origin_inside
};
let mut previous = 0.0_f64;
let mut mass = 0.0_f64;
for hit in hits {
if inside {
mass += (-0.5 * previous * previous).exp() - (-0.5 * hit * hit).exp();
}
inside = !inside;
previous = hit;
}
if inside {
mass += (-0.5 * previous * previous).exp();
}
mass.clamp(0.0, 1.0)
}
fn ray_intersections(vertices: &[[f64; 2]], direction: [f64; 2]) -> Vec<f64> {
let mut hits = Vec::new();
for idx in 0..vertices.len() {
let p = vertices[idx];
let s = sub2(vertices[(idx + 1) % vertices.len()], p);
let denom = cross2(direction, s);
if denom.abs() <= RAY_EPS {
continue;
}
let ray_t = cross2(p, s) / denom;
let segment_t = cross2(p, direction) / denom;
if ray_t > RAY_EPS && (-RAY_EPS..=1.0 + RAY_EPS).contains(&segment_t) {
hits.push(ray_t);
}
}
hits
}
fn rank_one_probability(
vertices: &[[f64; 2]],
origin_inside: bool,
origin_on_boundary: bool,
direction: [f64; 2],
) -> f64 {
if norm2(direction) == 0.0 {
return if origin_on_boundary || origin_inside {
1.0
} else {
0.0
};
}
let mut hits = line_intersections(vertices, direction);
sort_dedup(&mut hits);
if hits.is_empty() {
if origin_on_boundary {
let plus_inside = point_in_polygon_planar(vertices, scale2(direction, RAY_PROBE));
let minus_inside = point_in_polygon_planar(vertices, scale2(direction, -RAY_PROBE));
return match (minus_inside, plus_inside) {
(true, true) => 1.0,
(false, false) => 0.0,
_ => 0.5,
};
}
return if origin_inside { 1.0 } else { 0.0 };
}
let mut probability = 0.0;
for idx in 0..=hits.len() {
let lo = if idx == 0 {
f64::NEG_INFINITY
} else {
hits[idx - 1]
};
let hi = if idx == hits.len() {
f64::INFINITY
} else {
hits[idx]
};
let probe = if lo.is_infinite() {
hi - 1.0
} else if hi.is_infinite() {
lo + 1.0
} else {
0.5 * (lo + hi)
};
let point = scale2(direction, probe);
if point_in_polygon_planar(vertices, point) {
probability += normal_interval_probability(lo, hi);
}
}
probability.clamp(0.0, 1.0)
}
fn line_intersections(vertices: &[[f64; 2]], direction: [f64; 2]) -> Vec<f64> {
let mut hits = Vec::new();
for idx in 0..vertices.len() {
let p = vertices[idx];
let s = sub2(vertices[(idx + 1) % vertices.len()], p);
let denom = cross2(direction, s);
if denom.abs() <= RAY_EPS {
continue;
}
let line_t = cross2(p, s) / denom;
let segment_t = cross2(p, direction) / denom;
if (-RAY_EPS..=1.0 + RAY_EPS).contains(&segment_t) {
hits.push(line_t);
}
}
hits
}
fn normal_interval_probability(lo: f64, hi: f64) -> f64 {
let cdf_hi = if hi.is_infinite() && hi.is_sign_positive() {
1.0
} else {
normal_cdf(hi)
};
let cdf_lo = if lo.is_infinite() && lo.is_sign_negative() {
0.0
} else {
normal_cdf(lo)
};
cdf_hi - cdf_lo
}
fn integrate_gl64<F>(a: f64, b: f64, mut f: F) -> f64
where
F: FnMut(f64) -> f64,
{
let center = 0.5 * (a + b);
let half = 0.5 * (b - a);
let mut sum = 0.0;
for idx in 0..GL64_POSITIVE_NODES.len() {
let node = GL64_POSITIVE_NODES[idx];
let weight = GL64_POSITIVE_WEIGHTS[idx];
sum += weight * (f(center - half * node) + f(center + half * node));
}
half * sum
}
fn normal_cdf(x: f64) -> f64 {
0.5 * (1.0 + erf(x * core::f64::consts::FRAC_1_SQRT_2))
}
fn sort_dedup(values: &mut Vec<f64>) {
values.sort_by(f64::total_cmp);
let mut deduped = Vec::with_capacity(values.len());
for &value in values.iter() {
if deduped
.last()
.is_none_or(|last: &f64| (value - *last).abs() > DEDUP_EPS)
{
deduped.push(value);
}
}
*values = deduped;
}
fn wrap_pi(angle: f64) -> f64 {
let wrapped = (angle + core::f64::consts::PI).rem_euclid(TWO_PI) - core::f64::consts::PI;
if wrapped == -core::f64::consts::PI {
core::f64::consts::PI
} else {
wrapped
}
}
fn add2(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
[a[0] + b[0], a[1] + b[1]]
}
fn sub2(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
[a[0] - b[0], a[1] - b[1]]
}
fn scale2(a: [f64; 2], scale: f64) -> [f64; 2] {
[a[0] * scale, a[1] * scale]
}
fn dot2(a: [f64; 2], b: [f64; 2]) -> f64 {
a[0] * b[0] + a[1] * b[1]
}
fn cross2(a: [f64; 2], b: [f64; 2]) -> f64 {
a[0] * b[1] - a[1] * b[0]
}
fn norm2(a: [f64; 2]) -> f64 {
dot2(a, a).sqrt()
}