#[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::{self, Geom};
#[allow(dead_code)]
fn out(geometry: Geometry<f64>, srid: i32, func: &'static str) -> Result<Vec<u8>> {
geom::encode_canonical_gpb(
&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);
}
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)
}
#[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,
)
}
#[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,
)
}
#[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 = "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());
}
}