use geozero::{GeomProcessor, GeozeroGeometry};
use crate::error::{Error, Result};
use crate::geom;
use crate::gpb;
#[derive(Default)]
struct Ordinates {
z: Vec<f64>,
m: Vec<f64>,
dims: geozero::CoordDimensions,
}
impl GeomProcessor for Ordinates {
fn dimensions(&self) -> geozero::CoordDimensions {
geozero::CoordDimensions {
z: true,
m: true,
t: false,
tm: false,
}
}
fn coordinate(
&mut self,
_x: f64,
_y: f64,
z: Option<f64>,
m: Option<f64>,
_t: Option<f64>,
_tm: Option<u64>,
_idx: usize,
) -> geozero::error::Result<()> {
if let Some(z) = z {
self.z.push(z);
self.dims.z = true;
}
if let Some(m) = m {
self.m.push(m);
self.dims.m = true;
}
Ok(())
}
}
fn ordinates(bytes: &[u8], func: &'static str) -> Result<Ordinates> {
let wkb: &[u8] = if gpb::is_gpb(bytes) {
let header = gpb::GpbHeader::parse(bytes)?;
&bytes[header.wkb_offset..]
} else {
bytes
};
let mut sink = Ordinates::default();
geozero::wkb::Wkb(wkb.to_vec())
.process_geom(&mut sink)
.map_err(|e| Error::Unsupported {
func,
reason: format!("could not read the encoded coordinates: {e}"),
})?;
Ok(sink)
}
pub fn st_has_z(bytes: &[u8]) -> Result<bool> {
if crate::functions::surface::z_extent(bytes)?.is_some() {
return Ok(true);
}
Ok(!ordinates(bytes, "ST_HasZ")?.z.is_empty())
}
pub fn st_has_m(bytes: &[u8]) -> Result<bool> {
if crate::geom::surface_kind(bytes).is_some() {
return Ok(false); }
Ok(!ordinates(bytes, "ST_HasM")?.m.is_empty())
}
pub fn st_coord_dim(bytes: &[u8]) -> Result<i64> {
if let Some(extent) = crate::functions::surface::z_extent(bytes)? {
let _ = extent;
return Ok(3);
}
let o = ordinates(bytes, "ST_CoordDim")?;
Ok(2 + i64::from(!o.z.is_empty()) + i64::from(!o.m.is_empty()))
}
pub fn st_z(bytes: &[u8]) -> Result<Option<f64>> {
single_ordinate(bytes, "ST_Z", |o| &o.z)
}
pub fn st_m(bytes: &[u8]) -> Result<Option<f64>> {
single_ordinate(bytes, "ST_M", |o| &o.m)
}
fn single_ordinate(
bytes: &[u8],
func: &'static str,
pick: impl Fn(&Ordinates) -> &Vec<f64>,
) -> Result<Option<f64>> {
let g = geom::decode_auto(bytes)?;
if !matches!(g.geometry, geo_types::Geometry::Point(_)) {
return Err(Error::Unsupported {
func,
reason: "argument must be a POINT".into(),
});
}
let o = ordinates(bytes, func)?;
Ok(pick(&o).first().copied())
}
pub fn st_zmin(bytes: &[u8]) -> Result<Option<f64>> {
z_extent(bytes, "ST_ZMin", f64::min)
}
pub fn st_zmax(bytes: &[u8]) -> Result<Option<f64>> {
z_extent(bytes, "ST_ZMax", f64::max)
}
fn z_extent(bytes: &[u8], func: &'static str, pick: fn(f64, f64) -> f64) -> Result<Option<f64>> {
if let Some((lo, hi)) = crate::functions::surface::z_extent(bytes)? {
return Ok(Some(pick(lo, hi)));
}
let g = geom::decode_auto(bytes)?;
if geom::is_empty(&g.geometry) {
return Ok(None);
}
let o = ordinates(bytes, func)?;
Ok(Some(
o.z.iter()
.copied()
.fold(None, |acc: Option<f64>, v| {
Some(acc.map_or(v, |a| pick(a, v)))
})
.unwrap_or(0.0),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::functions::io::{st_as_text, st_geom_from_text, st_set_srid, st_srid};
fn point_z() -> Vec<u8> {
let mut v = vec![0x01];
v.extend_from_slice(&1001u32.to_le_bytes());
for value in [1.0f64, 2.0, 3.0] {
v.extend_from_slice(&value.to_le_bytes());
}
v
}
fn line_z() -> Vec<u8> {
let mut v = vec![0x01];
v.extend_from_slice(&1002u32.to_le_bytes());
v.extend_from_slice(&2u32.to_le_bytes());
for value in [0.0f64, 0.0, 10.0, 1.0, 1.0, 30.0] {
v.extend_from_slice(&value.to_le_bytes());
}
v
}
#[test]
fn dimensionality_is_reported_from_the_encoding() {
assert!(st_has_z(&point_z()).unwrap());
assert!(!st_has_m(&point_z()).unwrap());
assert_eq!(st_coord_dim(&point_z()).unwrap(), 3);
let flat = st_geom_from_text("POINT(1 2)", None).unwrap();
assert!(!st_has_z(&flat).unwrap());
assert_eq!(st_coord_dim(&flat).unwrap(), 2);
}
#[test]
fn z_ordinates_are_readable_even_though_the_geometry_is_2d() {
assert_eq!(st_z(&point_z()).unwrap(), Some(3.0));
assert_eq!(
st_z(&st_geom_from_text("POINT(1 2)", None).unwrap()).unwrap(),
None
);
assert_eq!(st_zmin(&line_z()).unwrap(), Some(10.0));
assert_eq!(st_zmax(&line_z()).unwrap(), Some(30.0));
assert!(st_z(&line_z()).is_err());
let flat = st_geom_from_text("LINESTRING(0 0,1 1)", None).unwrap();
assert_eq!(st_zmax(&flat).unwrap(), Some(0.0));
assert_eq!(
st_zmax(&st_geom_from_text("LINESTRING EMPTY", None).unwrap()).unwrap(),
None
);
}
#[test]
fn a_3d_payload_survives_storage_unchanged() {
let labelled = st_set_srid(&point_z(), 6697).unwrap();
assert_eq!(st_srid(&labelled).unwrap(), 6697);
assert_eq!(st_z(&labelled).unwrap(), Some(3.0));
assert_eq!(st_coord_dim(&labelled).unwrap(), 3);
assert!(st_as_text(&labelled).is_err());
let flat = crate::functions::compat::st_force_2d(&labelled).unwrap();
assert_eq!(st_as_text(&flat).unwrap(), "POINT(1 2)");
assert_eq!(st_z(&flat).unwrap(), None);
}
#[test]
fn planar_work_still_works_on_3d_input() {
let p = point_z();
assert_eq!(crate::functions::rtree::st_min_x(&p).unwrap(), Some(1.0));
let window = st_geom_from_text("POLYGON((0 0,4 0,4 4,0 4,0 0))", None).unwrap();
assert!(crate::functions::predicates::st_intersects(&p, &window).unwrap());
}
}