#[cfg(feature = "concave-hull")]
use geo::algorithm::{Area, ConcaveHull, ConvexHull};
use geo_types::Geometry;
#[allow(unused_imports)]
use crate::error::{Error, Result};
use crate::geom;
#[allow(dead_code)]
fn out(
geometry: Geometry<f64>,
srid: i32,
func: &'static str,
sources: &[&[u8]],
) -> Result<Vec<u8>> {
geom::encode_derived(geometry, srid, func, sources)
}
#[cfg(feature = "voronoi")]
fn out_2d(geometry: Geometry<f64>, srid: i32, func: &'static str) -> Result<Vec<u8>> {
geom::encode_canonical_gpb(
&crate::geom::Geom {
geometry,
srid,
has_zm: false,
},
func,
)
}
#[cfg(feature = "concave-hull")]
pub fn st_concave_hull(bytes: &[u8], target_percent: f64) -> Result<Vec<u8>> {
const FUNC: &str = "ST_ConcaveHull";
if !(0.0..=1.0).contains(&target_percent) {
return Err(Error::Unsupported {
func: FUNC,
reason: format!(
"target_percent must be between 0 and 1 (got {target_percent}); it is the \
fraction of the convex hull's area to aim for, as in PostGIS"
),
});
}
let g = geom::decode_auto(bytes)?;
let convex = convex_hull_of(&g.geometry, FUNC)?;
let convex_area = convex.unsigned_area();
if target_percent >= 1.0 || convex_area == 0.0 {
return out(Geometry::Polygon(convex), g.srid, FUNC, &[bytes]);
}
let target_area = convex_area * target_percent;
let (mut lo, mut hi) = (0.01_f64, 100.0_f64);
let mut best = convex.clone();
for _ in 0..24 {
let mid = (lo * hi).sqrt(); let hull = concave_hull_of(&g.geometry, mid, FUNC)?;
if hull.unsigned_area() >= target_area {
best = hull;
hi = mid;
} else {
lo = mid;
}
if hi / lo < 1.001 {
break;
}
}
out(Geometry::Polygon(best), g.srid, FUNC, &[bytes])
}
#[cfg(feature = "concave-hull")]
fn convex_hull_of(g: &Geometry<f64>, func: &'static str) -> Result<geo_types::Polygon<f64>> {
Ok(match g {
Geometry::MultiPoint(mp) => mp.convex_hull(),
Geometry::LineString(l) => l.convex_hull(),
Geometry::MultiLineString(mls) => mls.convex_hull(),
Geometry::Polygon(p) => p.convex_hull(),
Geometry::MultiPolygon(mp) => mp.convex_hull(),
Geometry::Point(p) => p.convex_hull(),
_ => {
return Err(Error::Unsupported {
func,
reason: "unsupported geometry type".into(),
});
}
})
}
#[cfg(feature = "concave-hull")]
fn concave_hull_of(
g: &Geometry<f64>,
concavity: f64,
func: &'static str,
) -> Result<geo_types::Polygon<f64>> {
let options = geo::algorithm::concave_hull::ConcaveHullOptions {
concavity,
length_threshold: 0.0,
};
Ok(match g {
Geometry::MultiPoint(mp) => mp.concave_hull_with_options(options),
Geometry::LineString(l) => l.concave_hull_with_options(options),
Geometry::MultiLineString(mls) => mls.concave_hull_with_options(options),
Geometry::Polygon(p) => p.concave_hull_with_options(options),
Geometry::MultiPolygon(mp) => mp.concave_hull_with_options(options),
Geometry::Point(_) => convex_hull_of(g, func)?,
_ => {
return Err(Error::Unsupported {
func,
reason: "unsupported geometry type".into(),
});
}
})
}
#[cfg(feature = "delaunay")]
pub fn st_delaunay_triangles(bytes: &[u8]) -> Result<Vec<u8>> {
use geo::algorithm::TriangulateDelaunayUnconstrained;
const FUNC: &str = "ST_DelaunayTriangles";
let g = geom::decode_auto(bytes)?;
let triangles = match &g.geometry {
Geometry::MultiPoint(mp) => mp.unconstrained_triangulation(),
Geometry::LineString(l) => l.unconstrained_triangulation(),
Geometry::MultiLineString(mls) => mls.unconstrained_triangulation(),
Geometry::Polygon(p) => p.unconstrained_triangulation(),
Geometry::MultiPolygon(mp) => mp.unconstrained_triangulation(),
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "unsupported geometry type".into(),
});
}
}
.map_err(|e| Error::Unsupported {
func: FUNC,
reason: format!("triangulation failed: {e:?}"),
})?;
let polygons: Vec<geo_types::Polygon<f64>> =
triangles.into_iter().map(|t| t.to_polygon()).collect();
out(
Geometry::MultiPolygon(geo_types::MultiPolygon::new(polygons)),
g.srid,
FUNC,
&[bytes],
)
}
#[cfg(feature = "delaunay")]
pub fn st_triangulate_polygon(bytes: &[u8]) -> Result<Vec<u8>> {
use geo::algorithm::TriangulateDelaunay;
const FUNC: &str = "ST_TriangulatePolygon";
let g = geom::decode_auto(bytes)?;
let triangles = match &g.geometry {
Geometry::Polygon(p) => p.constrained_triangulation(Default::default()),
Geometry::MultiPolygon(mp) => mp.constrained_triangulation(Default::default()),
Geometry::Rect(r) => r.to_polygon().constrained_triangulation(Default::default()),
Geometry::Triangle(t) => t.to_polygon().constrained_triangulation(Default::default()),
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "argument must be a POLYGON or MULTIPOLYGON (PostGIS answers \
GEOMETRYCOLLECTION EMPTY here; kenro will not return a collection, \
and an empty result would hide the mistake)"
.into(),
});
}
}
.map_err(|e| Error::Unsupported {
func: FUNC,
reason: format!("triangulation failed: {e:?}"),
})?;
out(
Geometry::MultiPolygon(geo_types::MultiPolygon::new(
triangles.into_iter().map(|t| t.to_polygon()).collect(),
)),
g.srid,
FUNC,
&[bytes],
)
}
#[cfg(feature = "voronoi")]
fn voronoi_clip(
geometry: &Geometry<f64>,
extend_to: Option<&Geometry<f64>>,
) -> Option<geo_types::Polygon<f64>> {
use geo::BoundingRect;
let rect = geometry.bounding_rect()?;
let pad = rect.width().max(rect.height());
let (mut min_x, mut min_y) = (rect.min().x - pad, rect.min().y - pad);
let (mut max_x, mut max_y) = (rect.max().x + pad, rect.max().y + pad);
if let Some(e) = extend_to.and_then(|e| e.bounding_rect()) {
min_x = min_x.min(e.min().x);
min_y = min_y.min(e.min().y);
max_x = max_x.max(e.max().x);
max_y = max_y.max(e.max().y);
}
Some(
geo_types::Rect::new(
geo_types::coord! { x: min_x, y: min_y },
geo_types::coord! { x: max_x, y: max_y },
)
.to_polygon(),
)
}
#[cfg(feature = "voronoi")]
pub fn st_voronoi_polygons(
bytes: &[u8],
tolerance: Option<f64>,
extend_to: Option<&[u8]>,
) -> Result<Vec<u8>> {
use geo::algorithm::voronoi::{Voronoi, VoronoiClip, VoronoiError, VoronoiParams};
const FUNC: &str = "ST_VoronoiPolygons";
let g = geom::decode_auto(bytes)?;
let extend = extend_to.map(geom::decode_auto).transpose()?;
let empty = || {
out_2d(
Geometry::MultiPolygon(geo_types::MultiPolygon::new(vec![])),
g.srid,
FUNC,
)
};
let Some(clip) = voronoi_clip(&g.geometry, extend.as_ref().map(|e| &e.geometry)) else {
return empty();
};
let params = VoronoiParams::new()
.tolerance(tolerance.unwrap_or(0.0))
.clip(VoronoiClip::Polygon(&clip));
let cells = match &g.geometry {
Geometry::MultiPoint(mp) => mp.voronoi_cells_with_params(params),
Geometry::Point(p) => p.voronoi_cells_with_params(params),
Geometry::LineString(l) => l.voronoi_cells_with_params(params),
Geometry::MultiLineString(mls) => mls.voronoi_cells_with_params(params),
Geometry::Polygon(p) => p.voronoi_cells_with_params(params),
Geometry::MultiPolygon(mp) => mp.voronoi_cells_with_params(params),
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "unsupported geometry type".into(),
});
}
};
let cells = match cells {
Ok(c) => c,
Err(VoronoiError::InsufficientVertices) => return empty(),
Err(VoronoiError::CollinearInput) => {
return Err(Error::Unsupported {
func: FUNC,
reason: "the input vertices are collinear, so they bound no cells; PostGIS \
returns degenerate ones here, kenro does not — use ST_VoronoiLines \
for the perpendicular bisectors"
.into(),
});
}
Err(e) => {
return Err(Error::Unsupported {
func: FUNC,
reason: format!("voronoi failed: {e:?}"),
});
}
};
out_2d(
Geometry::MultiPolygon(geo_types::MultiPolygon::new(cells)),
g.srid,
FUNC,
)
}
#[cfg(feature = "voronoi")]
pub fn st_voronoi_lines(
bytes: &[u8],
tolerance: Option<f64>,
extend_to: Option<&[u8]>,
) -> Result<Vec<u8>> {
use geo::algorithm::voronoi::{Voronoi, VoronoiClip, VoronoiError, VoronoiParams};
const FUNC: &str = "ST_VoronoiLines";
let g = geom::decode_auto(bytes)?;
let extend = extend_to.map(geom::decode_auto).transpose()?;
let empty = || {
out_2d(
Geometry::MultiLineString(geo_types::MultiLineString::new(vec![])),
g.srid,
FUNC,
)
};
let Some(clip) = voronoi_clip(&g.geometry, extend.as_ref().map(|e| &e.geometry)) else {
return empty();
};
let params = VoronoiParams::new()
.tolerance(tolerance.unwrap_or(0.0))
.clip(VoronoiClip::Polygon(&clip));
let edges = match &g.geometry {
Geometry::MultiPoint(mp) => mp.voronoi_edges_with_params(params),
Geometry::Point(p) => p.voronoi_edges_with_params(params),
Geometry::LineString(l) => l.voronoi_edges_with_params(params),
Geometry::MultiLineString(mls) => mls.voronoi_edges_with_params(params),
Geometry::Polygon(p) => p.voronoi_edges_with_params(params),
Geometry::MultiPolygon(mp) => mp.voronoi_edges_with_params(params),
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "unsupported geometry type".into(),
});
}
};
let edges = match edges {
Ok(e) => e,
Err(VoronoiError::InsufficientVertices) => return empty(),
Err(e) => {
return Err(Error::Unsupported {
func: FUNC,
reason: format!("voronoi failed: {e:?}"),
});
}
};
out_2d(
Geometry::MultiLineString(geo_types::MultiLineString::new(
edges
.into_iter()
.map(|l| geo_types::LineString::new(vec![l.start, l.end]))
.collect(),
)),
g.srid,
FUNC,
)
}
#[cfg(test)]
mod tests {
#[cfg(any(feature = "concave-hull", feature = "delaunay"))]
use super::*;
#[cfg(any(feature = "concave-hull", feature = "delaunay"))]
use crate::functions::io::st_geom_from_text;
#[cfg(any(feature = "concave-hull", feature = "delaunay"))]
fn g(wkt: &str) -> Vec<u8> {
st_geom_from_text(wkt, None).unwrap()
}
#[cfg(feature = "concave-hull")]
const RING: &str = "MULTIPOINT(0 0,2 0,4 0,4 2,4 4,2 4,0 4,0 2,\
1 1,3 1,3 3,1 3)";
#[cfg(feature = "concave-hull")]
#[test]
fn concave_hull_keeps_postgis_argument_contract() {
use crate::functions::accessors::st_area;
let input = g(RING);
let convex = crate::functions::processing::st_convex_hull(&input).unwrap();
let convex_area = st_area(&convex).unwrap();
let at_one = st_concave_hull(&input, 1.0).unwrap();
assert!((st_area(&at_one).unwrap() - convex_area).abs() < 1e-9);
let mut previous = convex_area;
for target in [0.9, 0.7, 0.5, 0.2] {
let area = st_area(&st_concave_hull(&input, target).unwrap()).unwrap();
assert!(
area <= convex_area + 1e-9,
"{target}: {area} > {convex_area}"
);
assert!(area <= previous + 1e-9, "{target}: {area} > {previous}");
previous = area;
}
}
#[cfg(feature = "concave-hull")]
#[test]
fn concave_hull_rejects_a_geo_style_concavity() {
let err = st_concave_hull(&g(RING), 2.0).unwrap_err().to_string();
assert!(err.contains("between 0 and 1"), "{err}");
assert!(st_concave_hull(&g(RING), -0.1).is_err());
}
#[cfg(feature = "delaunay")]
#[test]
fn delaunay_triangulates_a_square_like_postgis() {
use crate::functions::accessors::{st_area, st_num_geometries};
let triangles = st_delaunay_triangles(&g("MULTIPOINT(0 0,4 0,4 4,0 4)")).unwrap();
assert_eq!(st_num_geometries(&triangles).unwrap(), 2);
assert!((st_area(&triangles).unwrap() - 16.0).abs() < 1e-9);
assert_eq!(
crate::functions::accessors::st_geometry_type(&triangles).unwrap(),
"ST_MultiPolygon"
);
}
#[cfg(feature = "voronoi")]
#[test]
fn voronoi_clips_the_way_postgis_does() {
use crate::functions::accessors::{st_area, st_geometry_type, st_num_geometries};
use crate::functions::io::st_as_text;
let square = g("MULTIPOINT(0 0,4 0,4 4,0 4)");
let v = st_voronoi_polygons(&square, None, None).unwrap();
assert_eq!(st_num_geometries(&v).unwrap(), 4);
assert!(
(st_area(&v).unwrap() - 144.0).abs() < 1e-9,
"{}",
st_area(&v).unwrap()
);
assert_eq!(st_geometry_type(&v).unwrap(), "ST_MultiPolygon");
let wide = g("MULTIPOINT(0 0,10 0,10 2,0 2)");
let v = st_voronoi_polygons(&wide, None, None).unwrap();
assert!(
(st_area(&v).unwrap() - 30.0 * 22.0).abs() < 1e-9,
"{}",
st_area(&v).unwrap()
);
let big = st_voronoi_polygons(
&square,
Some(0.0),
Some(&g("POLYGON((-10 -10,10 -10,10 10,-10 10,-10 -10))")),
)
.unwrap();
assert!(
(st_area(&big).unwrap() - 400.0).abs() < 1e-9,
"{}",
st_area(&big).unwrap()
);
let small = st_voronoi_polygons(
&square,
Some(0.0),
Some(&g("POLYGON((1 1,3 1,3 3,1 3,1 1))")),
)
.unwrap();
assert!(
(st_area(&small).unwrap() - 144.0).abs() < 1e-9,
"{}",
st_area(&small).unwrap()
);
assert_eq!(
st_as_text(&st_voronoi_polygons(&g("POINT(0 0)"), None, None).unwrap()).unwrap(),
"MULTIPOLYGON EMPTY"
);
let l = st_voronoi_lines(&square, None, None).unwrap();
assert_eq!(st_geometry_type(&l).unwrap(), "ST_MultiLineString");
assert!(st_num_geometries(&l).unwrap() >= 4);
let line = g("MULTIPOINT(0 0,1 1,2 2)");
let err = st_voronoi_polygons(&line, None, None)
.unwrap_err()
.to_string();
assert!(err.contains("ST_VoronoiLines"), "{err}");
assert!(st_voronoi_lines(&line, None, None).is_ok());
let mut z = vec![0x01];
z.extend_from_slice(&1004u32.to_le_bytes()); z.extend_from_slice(&4u32.to_le_bytes());
for xyz in [
[0.0_f64, 0.0, 5.0],
[4.0, 0.0, 6.0],
[4.0, 4.0, 7.0],
[0.0, 4.0, 8.0],
] {
z.push(0x01);
z.extend_from_slice(&1001u32.to_le_bytes()); for v in xyz {
z.extend_from_slice(&v.to_le_bytes());
}
}
use crate::functions::threed::st_has_z;
assert!(st_has_z(&z).unwrap(), "the fixture itself must be 3D");
assert!(!st_has_z(&st_voronoi_polygons(&z, None, None).unwrap()).unwrap());
assert!(!st_has_z(&st_voronoi_lines(&z, None, None).unwrap()).unwrap());
assert!(st_has_z(&st_delaunay_triangles(&z).unwrap()).unwrap());
}
#[cfg(feature = "delaunay")]
#[test]
fn constrained_triangulation_respects_holes_and_concavity() {
use crate::functions::accessors::{st_area, st_geometry_type, st_num_geometries};
let holed = g("POLYGON((0 0,10 0,10 10,0 10,0 0),(2 2,4 2,4 4,2 4,2 2))");
let t = st_triangulate_polygon(&holed).unwrap();
assert!((st_area(&t).unwrap() - 96.0).abs() < 1e-9);
assert_eq!(st_geometry_type(&t).unwrap(), "ST_MultiPolygon");
assert!((st_area(&st_delaunay_triangles(&holed).unwrap()).unwrap() - 100.0).abs() < 1e-9);
let l = g("POLYGON((0 0,10 0,10 4,4 4,4 10,0 10,0 0))");
assert!((st_area(&st_triangulate_polygon(&l).unwrap()).unwrap() - 64.0).abs() < 1e-9);
let two = g("MULTIPOLYGON(((0 0,1 0,1 1,0 0)),((5 5,6 5,6 6,5 5)))");
let t = st_triangulate_polygon(&two).unwrap();
assert_eq!(st_num_geometries(&t).unwrap(), 2);
assert!((st_area(&t).unwrap() - 1.0).abs() < 1e-9);
assert!(st_triangulate_polygon(&g("LINESTRING(0 0,1 1)")).is_err());
assert!(st_triangulate_polygon(&g("POINT(0 0)")).is_err());
}
}