use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::{AtomicU32, Ordering};
use parry3d::math::Vector;
use parry3d::query::PointQuery;
use parry3d::shape::ConvexPolyhedron;
use crate::coord::CoordSystem;
pub trait ZoneShape: Send + Sync {
fn contains_enu(&self, p: [f64; 3]) -> bool;
fn aabb_enu(&self) -> [f64; 6];
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum Zone {
Aabb { min: [f64; 3], max: [f64; 3] },
Cylinder { center: [f64; 2], radius_m: f64, z_min: f64, z_max: f64 },
ExtrudedPolygon { ring: Vec<[f64; 2]>, z_min: f64, z_max: f64 },
ConvexHull { vertices: Vec<[f64; 3]> },
}
#[cfg(feature = "geo")]
impl Zone {
pub fn from_geo_point(point: &geo_types::Point) -> Self {
Self::Aabb {
min: [point.x(), point.y(), 0.0],
max: [point.x() + 0.00001, point.y() + 0.00001, 1.0],
}
}
pub fn from_geo_polygon(poly: &geo_types::Polygon) -> Self {
let ring: Vec<[f64; 2]> = poly
.exterior()
.points()
.map(|c| [c.y(), c.x()])
.collect();
Self::ExtrudedPolygon { ring, z_min: 0.0, z_max: 0.0 }
}
pub fn from_geo_multi_polygon(multipoly: &geo_types::MultiPolygon) -> Vec<Self> {
multipoly.0.iter().map(Self::from_geo_polygon).collect()
}
}
impl std::fmt::Display for Zone {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Aabb { min, max } => write!(
f,
"AABB([{:.4}, {:.4}, {:.1}], [{:.4}, {:.4}, {:.1}])",
min[0], min[1], min[2], max[0], max[1], max[2]
),
Self::Cylinder { center, radius_m, z_min, z_max } => write!(
f,
"CYLINDER(({:.6}, {:.6}), {:.2}, [{:.1}, {:.1}])",
center[0], center[1], radius_m, z_min, z_max
),
Self::ExtrudedPolygon { ring, z_min, z_max } => {
write!(f, "POLYGON([")?;
for (i, v) in ring.iter().take(8).enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "({:.6}, {:.6})", v[0], v[1])?;
}
if ring.len() > 8 {
write!(f, ", ...")?;
}
write!(f, "], {:.1}, {:.1})", z_min, z_max)
}
Self::ConvexHull { vertices } => {
write!(f, "CONVEXHULL([")?;
for (i, v) in vertices.iter().take(6).enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "({:.4}, {:.4}, {:.2})", v[0], v[1], v[2])?;
}
if vertices.len() > 6 {
write!(f, ", ...")?;
}
write!(f, "])")
}
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct ZoneEntry {
pub id: u32,
pub zone: Zone,
#[serde(default)]
pub priority: u8,
#[serde(default)]
pub layer: u8,
}
impl ZoneEntry {
pub fn new(id: u32, zone: Zone) -> Self {
Self { id, zone, priority: 0, layer: 0 }
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
pub fn with_layer(mut self, layer: u8) -> Self {
self.layer = layer;
self
}
}
impl Zone {
pub fn cylinder(lat: f64, lon: f64, radius_m: f64) -> ZoneBuilder {
ZoneBuilder(Zone::Cylinder { center: [lat, lon], radius_m, z_min: 0.0, z_max: 0.0 })
}
pub fn aabb(min: [f64; 3], max: [f64; 3]) -> ZoneBuilder {
ZoneBuilder(Zone::Aabb { min, max })
}
pub fn extruded_polygon(ring: Vec<[f64; 2]>) -> ZoneBuilder {
ZoneBuilder(Zone::ExtrudedPolygon { ring, z_min: 0.0, z_max: 0.0 })
}
pub fn convex_hull(vertices: Vec<[f64; 3]>) -> ZoneBuilder {
ZoneBuilder(Zone::ConvexHull { vertices })
}
pub fn sphere(center: [f64; 3], radius_m: f64) -> Zone {
Zone::Aabb {
min: [center[0] - radius_m, center[1] - radius_m, center[2] - radius_m],
max: [center[0] + radius_m, center[1] + radius_m, center[2] + radius_m],
}
}
}
pub struct ZoneBuilder(pub Zone);
impl ZoneBuilder {
pub fn with_z_range(mut self, z_min: f64, z_max: f64) -> Self {
match &mut self.0 {
Zone::Cylinder { z_min: z1, z_max: z2, .. } => { *z1 = z_min; *z2 = z_max; }
Zone::ExtrudedPolygon { z_min: z1, z_max: z2, .. } => { *z1 = z_min; *z2 = z_max; }
_ => {}
}
self
}
pub fn entry(self, id: u32) -> ZoneEntry {
ZoneEntry { id, zone: self.0, priority: 0, layer: 0 }
}
pub fn entry_with_meta(self, id: u32, priority: u8, layer: u8) -> ZoneEntry {
ZoneEntry { id, zone: self.0, priority, layer }
}
pub fn build(self) -> Zone {
self.0
}
}
struct AabbEnu {
min: [f64; 3],
max: [f64; 3],
}
struct CylinderEnu {
cx: f64,
cy: f64,
r2: f64,
z_min: f64,
z_max: f64,
}
struct PolygonEnu {
ring: Vec<[f64; 2]>,
z_min: f64,
z_max: f64,
}
struct ConvexEnu {
hull: Option<ConvexPolyhedron>,
aabb: [f64; 6],
}
impl ZoneShape for AabbEnu {
fn contains_enu(&self, p: [f64; 3]) -> bool {
(0..3).all(|i| p[i] >= self.min[i] && p[i] <= self.max[i])
}
fn aabb_enu(&self) -> [f64; 6] {
[self.min[0], self.min[1], self.min[2], self.max[0], self.max[1], self.max[2]]
}
}
impl ZoneShape for CylinderEnu {
fn contains_enu(&self, p: [f64; 3]) -> bool {
let dx = p[0] - self.cx;
let dy = p[1] - self.cy;
dx * dx + dy * dy <= self.r2 && p[2] >= self.z_min && p[2] <= self.z_max
}
fn aabb_enu(&self) -> [f64; 6] {
let r = self.r2.sqrt();
[self.cx - r, self.cy - r, self.z_min, self.cx + r, self.cy + r, self.z_max]
}
}
impl ZoneShape for PolygonEnu {
fn contains_enu(&self, p: [f64; 3]) -> bool {
if p[2] < self.z_min || p[2] > self.z_max {
return false;
}
point_in_ring_enu(p[0], p[1], &self.ring)
}
fn aabb_enu(&self) -> [f64; 6] {
let min_x = self.ring.iter().map(|v| v[0]).fold(f64::MAX, f64::min);
let min_y = self.ring.iter().map(|v| v[1]).fold(f64::MAX, f64::min);
let max_x = self.ring.iter().map(|v| v[0]).fold(f64::MIN, f64::max);
let max_y = self.ring.iter().map(|v| v[1]).fold(f64::MIN, f64::max);
[min_x, min_y, self.z_min, max_x, max_y, self.z_max]
}
}
impl ZoneShape for ConvexEnu {
fn contains_enu(&self, p: [f64; 3]) -> bool {
match &self.hull {
Some(h) => h.contains_local_point(Vector::new(p[0] as f32, p[1] as f32, p[2] as f32)),
None => false,
}
}
fn aabb_enu(&self) -> [f64; 6] {
self.aabb
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PolygonError {
TooFewVertices,
NotClosed,
SelfIntersecting,
}
pub fn validate_polygon(ring: &[[f64; 2]]) -> Result<(), PolygonError> {
if ring.len() < 3 {
return Err(PolygonError::TooFewVertices);
}
let closed = ring.len() > 1
&& (ring[0][0] - ring[ring.len() - 1][0]).abs() < 1e-12
&& (ring[0][1] - ring[ring.len() - 1][1]).abs() < 1e-12;
let n = if closed { ring.len() - 1 } else { ring.len() };
if n < 3 {
return Err(PolygonError::TooFewVertices);
}
Ok(())
}
#[doc(hidden)]
pub fn point_in_ring_enu(px: f64, py: f64, ring: &[[f64; 2]]) -> bool {
let n = ring.len();
if n < 3 {
return false;
}
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = (ring[i][0], ring[i][1]);
let (xj, yj) = (ring[j][0], ring[j][1]);
if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
inside = !inside;
}
j = i;
}
inside
}
pub fn zone_to_shape<C: CoordSystem + ?Sized>(zone: &Zone, conv: &C) -> Box<dyn ZoneShape> {
match zone {
Zone::Aabb { min, max } => Box::new(AabbEnu {
min: conv.to_internal(*min),
max: conv.to_internal(*max),
}),
Zone::Cylinder { center, radius_m, z_min, z_max } => {
let c = conv.to_internal([center[0], center[1], 0.0]);
Box::new(CylinderEnu {
cx: c[0],
cy: c[1],
r2: radius_m * radius_m,
z_min: *z_min,
z_max: *z_max,
})
}
Zone::ExtrudedPolygon { ring, z_min, z_max } => {
let enu_ring = ring
.iter()
.map(|v| {
let e = conv.to_internal([v[0], v[1], 0.0]);
[e[0], e[1]]
})
.collect();
Box::new(PolygonEnu { ring: enu_ring, z_min: *z_min, z_max: *z_max })
}
Zone::ConvexHull { vertices } => {
let pts: Vec<Vector> = vertices
.iter()
.map(|v| {
let e = conv.to_internal(*v);
Vector::new(e[0] as f32, e[1] as f32, e[2] as f32)
})
.collect();
let (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]);
for v in &pts {
let arr = [v.x as f64, v.y as f64, v.z as f64];
for i in 0..3 {
lo[i] = lo[i].min(arr[i]);
hi[i] = hi[i].max(arr[i]);
}
}
let aabb = if pts.is_empty() {
[0.0; 6]
} else {
[lo[0], lo[1], lo[2], hi[0], hi[1], hi[2]]
};
Box::new(ConvexEnu { hull: ConvexPolyhedron::from_convex_hull(&pts), aabb })
}
}
}
pub struct ShrinkingZone {
pub cx: f64,
pub cy: f64,
pub z_min: f64,
pub z_max: f64,
pub radius: Arc<AtomicU32>, }
impl ShrinkingZone {
pub fn set_radius(&self, r: f64) {
self.radius.store((r * 1000.0) as u32, Ordering::Relaxed);
}
fn r(&self) -> f64 {
self.radius.load(Ordering::Relaxed) as f64 / 1000.0
}
}
impl ZoneShape for ShrinkingZone {
fn contains_enu(&self, p: [f64; 3]) -> bool {
let r = self.r();
let dx = p[0] - self.cx;
let dy = p[1] - self.cy;
dx * dx + dy * dy <= r * r && p[2] >= self.z_min && p[2] <= self.z_max
}
fn aabb_enu(&self) -> [f64; 6] {
let r = self.r();
[self.cx - r, self.cy - r, self.z_min, self.cx + r, self.cy + r, self.z_max]
}
}
pub struct FollowZone {
pub pos: Arc<RwLock<[f64; 3]>>,
pub radius_m: f64,
pub half_h: f64,
}
impl ZoneShape for FollowZone {
fn contains_enu(&self, p: [f64; 3]) -> bool {
let t = *self.pos.read().unwrap();
let dx = p[0] - t[0];
let dy = p[1] - t[1];
dx * dx + dy * dy <= self.radius_m * self.radius_m && (p[2] - t[2]).abs() <= self.half_h
}
fn aabb_enu(&self) -> [f64; 6] {
let t = *self.pos.read().unwrap();
let r = self.radius_m;
let h = self.half_h;
[t[0] - r, t[1] - r, t[2] - h, t[0] + r, t[1] + r, t[2] + h]
}
}
pub struct UnionZone {
pub parts: Vec<Box<dyn ZoneShape>>,
}
impl ZoneShape for UnionZone {
fn contains_enu(&self, p: [f64; 3]) -> bool {
self.parts.iter().any(|z| z.contains_enu(p))
}
fn aabb_enu(&self) -> [f64; 6] {
self.parts.iter().map(|z| z.aabb_enu()).fold(
[f64::MAX, f64::MAX, f64::MAX, f64::MIN, f64::MIN, f64::MIN],
|a, b| {
[
a[0].min(b[0]),
a[1].min(b[1]),
a[2].min(b[2]),
a[3].max(b[3]),
a[4].max(b[4]),
a[5].max(b[5]),
]
},
)
}
}
pub struct LambdaZone<F: Fn([f64; 3]) -> bool + Send + Sync> {
pub f: F,
pub aabb: [f64; 6],
}
impl<F: Fn([f64; 3]) -> bool + Send + Sync> ZoneShape for LambdaZone<F> {
fn contains_enu(&self, p: [f64; 3]) -> bool {
(self.f)(p)
}
fn aabb_enu(&self) -> [f64; 6] {
self.aabb
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coord::EnuConverter;
fn origin_conv() -> EnuConverter {
EnuConverter::new(0.0, 0.0, 0.0)
}
#[test]
fn cylinder_contains_and_excludes() {
let conv = origin_conv();
let shape = zone_to_shape(
&Zone::Cylinder { center: [0.0, 0.0], radius_m: 10.0, z_min: 0.0, z_max: 5.0 },
&conv,
);
assert!(shape.contains_enu([0.0, 0.0, 2.5]));
assert!(shape.contains_enu([9.9, 0.0, 0.0]));
assert!(!shape.contains_enu([10.1, 0.0, 2.5]), "outside radius");
assert!(!shape.contains_enu([0.0, 0.0, 6.0]), "above z_max");
}
#[test]
fn polygon_ray_crossing() {
let poly = PolygonEnu {
ring: vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]],
z_min: 0.0,
z_max: 3.0,
};
assert!(poly.contains_enu([5.0, 5.0, 1.0]));
assert!(!poly.contains_enu([11.0, 5.0, 1.0]));
assert!(!poly.contains_enu([5.0, 5.0, 9.0]), "z out of range");
}
#[test]
fn degenerate_ring_contains_nothing() {
assert!(!point_in_ring_enu(0.0, 0.0, &[]));
assert!(!point_in_ring_enu(0.0, 0.0, &[[0.0, 0.0], [1.0, 1.0]]));
}
#[test]
fn convex_hull_tetrahedron() {
let conv = origin_conv();
let shape = zone_to_shape(
&Zone::ConvexHull {
vertices: vec![[0.0, 0.0, 0.0], [0.0, 0.001, 0.0], [0.001, 0.0, 0.0], [0.0, 0.0, 50.0]],
},
&conv,
);
let bb = shape.aabb_enu();
assert!(bb[3] > bb[0] && bb[4] > bb[1] && bb[5] > bb[2], "non-degenerate aabb: {bb:?}");
assert!(!shape.contains_enu([1000.0, 1000.0, 1000.0]), "far point outside");
}
#[test]
fn union_zone_is_or() {
let a: Box<dyn ZoneShape> = Box::new(AabbEnu { min: [0.0; 3], max: [1.0, 1.0, 1.0] });
let b: Box<dyn ZoneShape> = Box::new(AabbEnu { min: [5.0, 5.0, 5.0], max: [6.0, 6.0, 6.0] });
let u = UnionZone { parts: vec![a, b] };
assert!(u.contains_enu([0.5, 0.5, 0.5]));
assert!(u.contains_enu([5.5, 5.5, 5.5]));
assert!(!u.contains_enu([3.0, 3.0, 3.0]));
let bb = u.aabb_enu();
assert_eq!(bb, [0.0, 0.0, 0.0, 6.0, 6.0, 6.0]);
}
#[test]
fn shrinking_zone_tracks_radius() {
let z = ShrinkingZone {
cx: 0.0,
cy: 0.0,
z_min: 0.0,
z_max: 10.0,
radius: Arc::new(AtomicU32::new(5000)), };
assert!(z.contains_enu([4.0, 0.0, 1.0]));
z.set_radius(2.0);
assert!(!z.contains_enu([4.0, 0.0, 1.0]));
assert!(z.contains_enu([1.0, 0.0, 1.0]));
}
#[test]
fn zone_entry_serde_round_trip() {
let entry = ZoneEntry::new(
42,
Zone::Cylinder { center: [10.7, 106.6], radius_m: 50.0, z_min: 0.0, z_max: 20.0 },
);
let json = serde_json::to_string(&entry).unwrap();
let back: ZoneEntry = serde_json::from_str(&json).unwrap();
assert_eq!(entry, back);
}
}