use crate::coords;
use crate::error::{Error, Result};
use crate::geom;
#[derive(Default)]
struct Parts {
points: Vec<[f64; 3]>,
segments: Vec<([f64; 3], [f64; 3])>,
faces: Vec<Vec<[f64; 3]>>,
}
impl Parts {
fn vertices(&self) -> impl Iterator<Item = [f64; 3]> + '_ {
self.points
.iter()
.copied()
.chain(self.segments.iter().flat_map(|(a, b)| [*a, *b]))
.chain(self.faces.iter().flatten().copied())
}
fn triangles(&self) -> impl Iterator<Item = ([f64; 3], [f64; 3], [f64; 3])> + '_ {
self.faces.iter().flat_map(|ring| {
let n = ring.len();
(1..n.saturating_sub(1)).map(move |i| (ring[0], ring[i], ring[i + 1]))
})
}
}
fn parts(bytes: &[u8], func: &'static str) -> Result<Parts> {
let mut parts = Parts::default();
let mut current: Vec<[f64; 3]> = Vec::new();
let mut current_base = 0u32;
let flush = |base: u32, run: &mut Vec<[f64; 3]>, parts: &mut Parts| {
if run.is_empty() {
return;
}
match base {
coords::base::POINT => parts.points.extend(run.iter().copied()),
coords::base::LINESTRING => {
for pair in run.windows(2) {
parts.segments.push((pair[0], pair[1]));
}
}
coords::base::POLYGON | coords::base::TRIANGLE => {
parts.faces.push(std::mem::take(run));
}
_ => {}
}
run.clear();
};
coords::for_each_coord_typed(bytes, &mut |c, first, base| {
if first {
flush(current_base, &mut current, &mut parts);
current_base = base;
}
current.push([c.x, c.y, c.z.unwrap_or(0.0)]);
})?;
flush(current_base, &mut current, &mut parts);
let _ = func;
Ok(parts)
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
[
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
]
}
fn norm(v: [f64; 3]) -> f64 {
dot(v, v).sqrt()
}
#[derive(Debug, Clone, Copy)]
struct Witness {
d: f64,
a: [f64; 3],
b: [f64; 3],
}
impl Witness {
fn between(a: [f64; 3], b: [f64; 3]) -> Self {
Self {
d: norm(sub(a, b)),
a,
b,
}
}
fn min(self, other: Self) -> Self {
if other.d < self.d { other } else { self }
}
fn flipped(self) -> Self {
Self {
d: self.d,
a: self.b,
b: self.a,
}
}
}
fn pt_seg(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> Witness {
let ab = sub(b, a);
let len_sq = dot(ab, ab);
let t = if len_sq == 0.0 {
0.0
} else {
(dot(sub(p, a), ab) / len_sq).clamp(0.0, 1.0)
};
Witness::between(p, lerp(a, b, t))
}
fn seg_seg(p1: [f64; 3], q1: [f64; 3], p2: [f64; 3], q2: [f64; 3]) -> Witness {
let (d1, d2, r) = (sub(q1, p1), sub(q2, p2), sub(p1, p2));
let (a, e, f) = (dot(d1, d1), dot(d2, d2), dot(d2, r));
if a <= f64::EPSILON && e <= f64::EPSILON {
return Witness::between(p1, p2);
}
if a <= f64::EPSILON {
return pt_seg(p1, p2, q2).flipped().flipped();
}
if e <= f64::EPSILON {
return pt_seg(p2, p1, q1).flipped();
}
let c = dot(d1, r);
let b = dot(d1, d2);
let denom = a * e - b * b;
let mut s = if denom != 0.0 {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.0 };
let mut t = (b * s + f) / e;
if t < 0.0 {
t = 0.0;
s = (-c / a).clamp(0.0, 1.0);
} else if t > 1.0 {
t = 1.0;
s = ((b - c) / a).clamp(0.0, 1.0);
}
Witness::between(lerp(p1, q1, s), lerp(p2, q2, t))
}
fn pt_tri(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> Witness {
let n = cross(sub(b, a), sub(c, a));
let n_sq = dot(n, n);
if n_sq > 0.0 {
let foot = {
let dist = dot(sub(p, a), n) / n_sq;
[p[0] - dist * n[0], p[1] - dist * n[1], p[2] - dist * n[2]]
};
let inside = [(a, b), (b, c), (c, a)]
.iter()
.all(|(u, v)| dot(cross(sub(*v, *u), sub(foot, *u)), n) >= 0.0);
if inside {
return Witness::between(p, foot);
}
}
pt_seg(p, a, b).min(pt_seg(p, b, c)).min(pt_seg(p, c, a))
}
fn tri_tri(t1: ([f64; 3], [f64; 3], [f64; 3]), t2: ([f64; 3], [f64; 3], [f64; 3])) -> Witness {
let e1 = [(t1.0, t1.1), (t1.1, t1.2), (t1.2, t1.0)];
let e2 = [(t2.0, t2.1), (t2.1, t2.2), (t2.2, t2.0)];
let mut best = Witness {
d: f64::INFINITY,
a: t1.0,
b: t2.0,
};
for (a1, b1) in e1 {
for (a2, b2) in e2 {
best = best.min(seg_seg(a1, b1, a2, b2));
if best.d == 0.0 {
return best;
}
}
}
for v in [t1.0, t1.1, t1.2] {
best = best.min(pt_tri(v, t2.0, t2.1, t2.2));
}
for v in [t2.0, t2.1, t2.2] {
best = best.min(pt_tri(v, t1.0, t1.1, t1.2).flipped());
}
best
}
fn closest(a: &Parts, b: &Parts, stop_at_zero: bool) -> Option<Witness> {
let mut best: Option<Witness> = None;
macro_rules! offer {
($w:expr) => {{
let w = $w;
best = Some(match best {
None => w,
Some(seen) => seen.min(w),
});
if stop_at_zero && best.is_some_and(|x| x.d == 0.0) {
return best;
}
}};
}
for &p in &a.points {
for &q in &b.points {
offer!(Witness::between(p, q));
}
for &(s, e) in &b.segments {
offer!(pt_seg(p, s, e));
}
for t in b.triangles() {
offer!(pt_tri(p, t.0, t.1, t.2));
}
}
for &(s, e) in &a.segments {
for &q in &b.points {
offer!(pt_seg(q, s, e).flipped());
}
for &(s2, e2) in &b.segments {
offer!(seg_seg(s, e, s2, e2));
}
for t in b.triangles() {
offer!(pt_tri(s, t.0, t.1, t.2));
offer!(pt_tri(e, t.0, t.1, t.2));
for (u, v) in [(t.0, t.1), (t.1, t.2), (t.2, t.0)] {
offer!(seg_seg(s, e, u, v));
}
}
}
for t1 in a.triangles() {
for &q in &b.points {
offer!(pt_tri(q, t1.0, t1.1, t1.2).flipped());
}
for &(s2, e2) in &b.segments {
offer!(pt_tri(s2, t1.0, t1.1, t1.2).flipped());
offer!(pt_tri(e2, t1.0, t1.1, t1.2).flipped());
for (u, v) in [(t1.0, t1.1), (t1.1, t1.2), (t1.2, t1.0)] {
offer!(seg_seg(u, v, s2, e2));
}
}
for t2 in b.triangles() {
offer!(tri_tri(t1, t2));
}
}
best
}
fn both(a: &[u8], b: &[u8], func: &'static str) -> Result<Option<(Parts, Parts)>> {
if !geom::has_z_encoded(a)? || !geom::has_z_encoded(b)? {
return Ok(None);
}
Ok(Some((parts(a, func)?, parts(b, func)?)))
}
pub fn st_3d_distance(a: &[u8], b: &[u8]) -> Result<Option<f64>> {
const FUNC: &str = "ST_3DDistance";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::predicates::st_distance(a, b);
};
Ok(closest(&pa, &pb, false).map(|w| w.d))
}
pub fn st_3d_dwithin(a: &[u8], b: &[u8], d: f64) -> Result<bool> {
const FUNC: &str = "ST_3DDWithin";
if d < 0.0 {
return Err(Error::Unsupported {
func: FUNC,
reason: "tolerance cannot be less than zero".into(),
});
}
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::predicates::st_dwithin(a, b, d);
};
Ok(closest(&pa, &pb, false).is_some_and(|w| w.d <= d))
}
pub fn st_3d_intersects(a: &[u8], b: &[u8]) -> Result<bool> {
const FUNC: &str = "ST_3DIntersects";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::predicates::st_intersects(a, b);
};
Ok(closest(&pa, &pb, true).is_some_and(|w| w.d == 0.0))
}
pub fn st_3d_max_distance(a: &[u8], b: &[u8]) -> Result<Option<f64>> {
const FUNC: &str = "ST_3DMaxDistance";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::linear::st_max_distance(a, b);
};
Ok(farthest(&pa, &pb).map(|w| w.d))
}
fn farthest(a: &Parts, b: &Parts) -> Option<Witness> {
let mut best: Option<Witness> = None;
for p in a.vertices() {
for q in b.vertices() {
let w = Witness::between(p, q);
best = Some(match best {
None => w,
Some(seen) if w.d > seen.d => w,
Some(seen) => seen,
});
}
}
best
}
pub fn st_3d_dfully_within(a: &[u8], b: &[u8], d: f64) -> Result<bool> {
if d < 0.0 {
return Err(Error::Unsupported {
func: "ST_3DDFullyWithin",
reason: "tolerance cannot be less than zero".into(),
});
}
Ok(st_3d_max_distance(a, b)?.is_some_and(|max| max <= d))
}
pub fn st_3d_closest_point(a: &[u8], b: &[u8]) -> Result<Option<Vec<u8>>> {
const FUNC: &str = "ST_3DClosestPoint";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::measures::st_closest_point(a, b);
};
let Some(w) = closest(&pa, &pb, false) else {
return Ok(None);
};
point_z(w.a, geom::srid_of(a)?, FUNC).map(Some)
}
pub fn st_3d_shortest_line(a: &[u8], b: &[u8]) -> Result<Option<Vec<u8>>> {
const FUNC: &str = "ST_3DShortestLine";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::linear::st_shortest_line(a, b);
};
let Some(w) = closest(&pa, &pb, false) else {
return Ok(None);
};
line_z(w.a, w.b, geom::srid_of(a)?, FUNC).map(Some)
}
pub fn st_3d_longest_line(a: &[u8], b: &[u8]) -> Result<Option<Vec<u8>>> {
const FUNC: &str = "ST_3DLongestLine";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::linear::st_longest_line(a, b);
};
let Some(w) = farthest(&pa, &pb) else {
return Ok(None);
};
line_z(w.a, w.b, geom::srid_of(a)?, FUNC).map(Some)
}
pub fn st_3d_line_interpolate_point(bytes: &[u8], fraction: f64) -> Result<Vec<u8>> {
const FUNC: &str = "ST_3DLineInterpolatePoint";
if !(0.0..=1.0).contains(&fraction) {
return Err(Error::Unsupported {
func: FUNC,
reason: "fraction must be between 0 and 1".into(),
});
}
let p = parts(bytes, FUNC)?;
if !p.points.is_empty() || !p.faces.is_empty() || p.segments.is_empty() {
return Err(Error::Unsupported {
func: FUNC,
reason: "the first argument must be a LINESTRING".into(),
});
}
let total: f64 = p.segments.iter().map(|(a, b)| norm(sub(*b, *a))).sum();
if total == 0.0 {
return point_z(p.segments[0].0, geom::srid_of(bytes)?, FUNC);
}
let target = fraction * total;
let mut walked = 0.0;
for (a, b) in &p.segments {
let len = norm(sub(*b, *a));
if walked + len >= target || (a, b) == p.segments.last().map(|(a, b)| (a, b)).unwrap() {
let t = if len == 0.0 {
0.0
} else {
((target - walked) / len).clamp(0.0, 1.0)
};
return point_z(lerp(*a, *b, t), geom::srid_of(bytes)?, FUNC);
}
walked += len;
}
unreachable!("the loop always returns on its last iteration")
}
fn point_z(p: [f64; 3], srid: i32, func: &'static str) -> Result<Vec<u8>> {
let index = coords::ZIndex::at(p[0], p[1], p[2]);
let g = geo_types::Geometry::Point(geo_types::Point::new(p[0], p[1]));
let wkb = coords::write_wkb_z(&g, &index, func)?;
Ok(crate::gpb::write_gpb(&wkb, srid, None, false))
}
fn line_z(a: [f64; 3], b: [f64; 3], srid: i32, func: &'static str) -> Result<Vec<u8>> {
let mut wkb = vec![0x01u8];
wkb.extend_from_slice(&1002u32.to_le_bytes());
wkb.extend_from_slice(&2u32.to_le_bytes());
for p in [a, b] {
for o in p {
wkb.extend_from_slice(&o.to_le_bytes());
}
}
let _ = func;
Ok(crate::gpb::write_gpb(&wkb, srid, None, false))
}
#[cfg(test)]
mod tests {
use super::*;
fn blob(ty: u32, counts: &[usize], coords: &[[f64; 3]]) -> Vec<u8> {
let mut v = vec![0x01u8];
v.extend_from_slice(&(1000 + ty).to_le_bytes());
for c in counts {
v.extend_from_slice(&(*c as u32).to_le_bytes());
}
for c in coords {
for o in c {
v.extend_from_slice(&o.to_le_bytes());
}
}
v
}
fn pt(x: f64, y: f64, z: f64) -> Vec<u8> {
blob(1, &[], &[[x, y, z]])
}
fn line(cs: &[[f64; 3]]) -> Vec<u8> {
blob(2, &[cs.len()], cs)
}
fn poly(cs: &[[f64; 3]]) -> Vec<u8> {
blob(3, &[1, cs.len()], cs)
}
fn square() -> Vec<u8> {
poly(&[
[0., 0., 0.],
[10., 0., 0.],
[10., 10., 0.],
[0., 10., 0.],
[0., 0., 0.],
])
}
fn near(got: Option<f64>, want: f64, what: &str) {
let g = got.unwrap_or(f64::NAN);
assert!((g - want).abs() < 1e-9, "{what}: got {g}, want {want}");
}
#[test]
fn distances_match_the_reference() {
near(
st_3d_distance(&pt(0., 0., 0.), &pt(1., 1., 1.)).unwrap(),
3f64.sqrt(),
"pt/pt",
);
near(
st_3d_distance(&pt(0., 0., 10.), &line(&[[0., 0., 0.], [10., 0., 0.]])).unwrap(),
10.0,
"pt/line",
);
near(
st_3d_distance(
&line(&[[0., 0., 0.], [10., 0., 0.]]),
&line(&[[5., -5., 4.], [5., 5., 4.]]),
)
.unwrap(),
4.0,
"line/line skew",
);
near(
st_3d_distance(&pt(5., 5., 10.), &square()).unwrap(),
10.0,
"pt/face interior",
);
near(
st_3d_distance(
&poly(&[[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 0., 0.]]),
&poly(&[[0., 0., 5.], [1., 0., 5.], [1., 1., 5.], [0., 0., 5.]]),
)
.unwrap(),
5.0,
"face/face parallel",
);
}
#[test]
fn a_closed_shell_has_no_interior() {
let cube = crate::functions::surface::fixtures::cube(6);
assert!(
!st_3d_intersects(&cube, &pt(0.5, 0.5, 0.5)).unwrap(),
"a point at the centre of a closed cube must not intersect it"
);
assert!(st_3d_intersects(&cube, &pt(0.5, 0.5, 0.0)).unwrap());
near(
st_3d_distance(&cube, &pt(0.5, 0.5, 3.0)).unwrap(),
2.0,
"cube/pt above",
);
}
#[test]
fn the_third_dimension_actually_separates() {
let (a, b) = (
line(&[[0., 0., 0.], [10., 0., 0.]]),
line(&[[5., -5., 4.], [5., 5., 4.]]),
);
assert!(!st_3d_intersects(&a, &b).unwrap());
assert!(crate::functions::predicates::st_intersects(&a, &b).unwrap());
assert!(st_3d_dwithin(&a, &b, 4.0).unwrap());
assert!(!st_3d_dwithin(&a, &b, 3.9).unwrap());
}
#[test]
fn max_distance_is_vertex_to_vertex() {
near(
st_3d_max_distance(&square(), &pt(0., 0., 0.)).unwrap(),
200f64.sqrt(),
"maxdist",
);
assert!(st_3d_dfully_within(&square(), &pt(0., 0., 0.), 14.15).unwrap());
assert!(!st_3d_dfully_within(&square(), &pt(0., 0., 0.), 14.14).unwrap());
}
#[test]
fn the_witnesses_are_the_measured_ones() {
use crate::functions::{rtree, threed};
let l = st_3d_shortest_line(
&line(&[[0., 0., 0.], [10., 0., 0.]]),
&line(&[[5., -5., 4.], [5., 5., 4.]]),
)
.unwrap()
.unwrap();
assert_eq!(threed::st_zmin(&l).unwrap(), Some(0.0));
assert_eq!(threed::st_zmax(&l).unwrap(), Some(4.0));
assert_eq!(rtree::st_min_x(&l).unwrap(), Some(5.0));
let p = st_3d_closest_point(&line(&[[0., 0., 0.], [10., 0., 0.]]), &pt(5., 0., 9.))
.unwrap()
.unwrap();
assert_eq!(threed::st_z(&p).unwrap(), Some(0.0));
assert_eq!(rtree::st_min_x(&p).unwrap(), Some(5.0));
}
#[test]
fn line_interpolate_takes_the_fraction_by_3d_length() {
use crate::functions::{rtree, threed};
let l = line(&[[0., 0., 0.], [10., 0., 10.], [20., 0., 30.]]);
let p = st_3d_line_interpolate_point(&l, 0.5).unwrap();
near(
rtree::st_min_x(&p).unwrap(),
11.837_722_339_831_622,
"3d lip x",
);
near(
threed::st_z(&p).unwrap(),
13.675_444_679_663_242,
"3d lip z",
);
}
#[test]
fn a_missing_z_delegates_to_the_2d_functions() {
let flat = crate::functions::io::st_geom_from_text("POINT(0 0)", None).unwrap();
near(
st_3d_distance(&pt(0., 0., 10.), &flat).unwrap(),
0.0,
"z vs 2d",
);
let two = crate::functions::io::st_geom_from_text("POINT(3 4)", None).unwrap();
near(st_3d_distance(&flat, &two).unwrap(), 5.0, "both 2d");
}
#[test]
fn a_vertical_wall_is_not_ambiguous_here() {
let wall = poly(&[
[0., 0., 0.],
[0., 0., 10.],
[10., 0., 10.],
[10., 0., 0.],
[0., 0., 0.],
]);
near(
st_3d_distance(&wall, &pt(0., 5., 5.)).unwrap(),
5.0,
"wall/pt",
);
}
}