pub(crate) const WKT3: &str = "https://w3id.org/rete/geo3#wktLiteral3D";
pub(crate) const BOX3D: &str = "https://w3id.org/rete/geo3#box3dLiteral";
const EPS: f64 = 1e-9;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Aabb {
pub min: [f64; 3],
pub max: [f64; 3],
}
#[derive(Clone, Copy)]
pub(crate) enum Rel3 {
Contains,
Within,
Adjacent,
}
const KEYWORDS: &[&str] = &[
"POINT",
"MULTIPOINT",
"LINESTRING",
"MULTILINESTRING",
"POLYGON",
"MULTIPOLYGON",
"POLYHEDRALSURFACE",
"TIN",
"GEOMETRYCOLLECTION",
"BOX3D",
];
pub(crate) fn parse(input: &str) -> Option<Aabb> {
let mut s = input.trim();
if let Some(rest) = s.strip_prefix('<') {
let end = rest.find('>')?;
s = rest[end + 1..].trim_start();
}
let open = s.find('(')?;
let head = s[..open].trim().to_ascii_uppercase();
let kw = head.split_whitespace().next()?;
if !KEYWORDS.contains(&kw) {
return None;
}
let close = s.rfind(')')?;
if close <= open {
return None;
}
let body = &s[open + 1..close];
let flat: String = body
.chars()
.map(|c| if c == '(' || c == ')' { ' ' } else { c })
.collect();
let mut mn = [f64::INFINITY; 3];
let mut mx = [f64::NEG_INFINITY; 3];
let mut any = false;
for group in flat.split(',') {
let mut ords = [0.0_f64; 3];
let mut nread = 0usize;
for tok in group.split_whitespace() {
if nread >= 3 {
break;
}
match tok.parse::<f64>() {
Ok(v) if v.is_finite() => {
ords[nread] = v;
nread += 1;
}
_ => {} }
}
if nread >= 2 {
for i in 0..3 {
mn[i] = mn[i].min(ords[i]);
mx[i] = mx[i].max(ords[i]);
}
any = true;
}
}
any.then_some(Aabb { min: mn, max: mx })
}
fn axis_gap(a_min: f64, a_max: f64, b_min: f64, b_max: f64) -> f64 {
(a_min - b_max).max(b_min - a_max).max(0.0)
}
pub(crate) fn distance(a: &Aabb, b: &Aabb) -> f64 {
let mut sum = 0.0;
for i in 0..3 {
let g = axis_gap(a.min[i], a.max[i], b.min[i], b.max[i]);
sum += g * g;
}
sum.sqrt()
}
fn contains(a: &Aabb, b: &Aabb) -> bool {
(0..3).all(|i| a.min[i] <= b.min[i] + EPS && a.max[i] >= b.max[i] - EPS)
}
fn adjacent(a: &Aabb, b: &Aabb, gap: f64) -> bool {
let g = gap.max(0.0);
(0..3).all(|i| a.min[i] <= b.max[i] + g + EPS && a.max[i] >= b.min[i] - g - EPS)
}
pub(crate) fn relate(rel: Rel3, a: &Aabb, b: &Aabb, gap: f64) -> bool {
match rel {
Rel3::Contains => contains(a, b),
Rel3::Within => contains(b, a),
Rel3::Adjacent => adjacent(a, b, gap),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> Aabb {
parse(s).unwrap()
}
#[test]
fn parse_point_box_multipoint() {
assert_eq!(
p("POINT Z(1 2 3)"),
Aabb {
min: [1.0, 2.0, 3.0],
max: [1.0, 2.0, 3.0]
}
);
assert_eq!(
p("BOX3D(0 0 0, 10 20 30)"),
Aabb {
min: [0.0, 0.0, 0.0],
max: [10.0, 20.0, 30.0]
}
);
assert_eq!(
p("MULTIPOINT Z(1 1 1, 5 -2 9)"),
Aabb {
min: [1.0, -2.0, 1.0],
max: [5.0, 1.0, 9.0]
}
);
assert_eq!(p("<urn:crs> POINT(4 5)").min, [4.0, 5.0, 0.0]);
let a = p("POLYHEDRALSURFACE Z(((0 0 0, 1 0 0, 1 1 2)))");
assert_eq!(a.min, [0.0, 0.0, 0.0]);
assert_eq!(a.max, [1.0, 1.0, 2.0]);
assert!(parse("FOO(1 2 3)").is_none());
assert!(parse("POINT EMPTY").is_none());
}
#[test]
fn distance_3d() {
let d = distance(&p("POINT Z(0 0 0)"), &p("POINT Z(0 0 5)"));
assert!((d - 5.0).abs() < 1e-9);
let d = distance(&p("POINT Z(0 0 0)"), &p("POINT Z(3 4 12)"));
assert!((d - 13.0).abs() < 1e-9);
assert_eq!(
distance(&p("BOX3D(0 0 0, 10 10 10)"), &p("POINT Z(5 5 5)")),
0.0
);
let d = distance(&p("BOX3D(0 0 0, 1 1 1)"), &p("BOX3D(0 0 4, 1 1 5)"));
assert!((d - 3.0).abs() < 1e-9);
}
#[test]
fn relations_3d() {
let big = p("BOX3D(0 0 0, 10 10 10)");
let small = p("BOX3D(2 2 2, 4 4 4)");
assert!(relate(Rel3::Contains, &big, &small, 0.0));
assert!(!relate(Rel3::Contains, &small, &big, 0.0));
assert!(relate(Rel3::Within, &small, &big, 0.0));
let a = p("BOX3D(0 0 0, 1 1 1)");
let b = p("BOX3D(0 0 3, 1 1 4)");
assert!(!relate(Rel3::Adjacent, &a, &b, 0.0));
assert!(relate(Rel3::Adjacent, &a, &b, 3.0));
let c = p("BOX3D(1 0 0, 2 1 1)");
assert!(relate(Rel3::Adjacent, &a, &c, 0.0));
}
}