use geo_types::{Coord, Geometry, LineString, MultiLineString, MultiPoint, Point, Polygon};
use crate::error::{Error, Result};
use crate::geom::{self, Geom};
fn out(geometry: Geometry<f64>, srid: i32, func: &'static str) -> Result<Vec<u8>> {
geom::encode_canonical_gpb(
&Geom {
geometry,
srid,
has_zm: false,
},
func,
)
}
fn as_polygon(g: &Geometry<f64>) -> Option<&Polygon<f64>> {
match g {
Geometry::Polygon(p) => Some(p),
_ => None,
}
}
fn as_line(g: &Geometry<f64>) -> Option<&LineString<f64>> {
match g {
Geometry::LineString(l) => Some(l),
_ => None,
}
}
pub fn st_exterior_ring(bytes: &[u8]) -> Result<Option<Vec<u8>>> {
let g = geom::decode_auto(bytes)?;
let Some(poly) = as_polygon(&g.geometry) else {
return Ok(None);
};
out(
Geometry::LineString(poly.exterior().clone()),
g.srid,
"ST_ExteriorRing",
)
.map(Some)
}
pub fn st_interior_ring_n(bytes: &[u8], n: i64) -> Result<Option<Vec<u8>>> {
let g = geom::decode_auto(bytes)?;
let Some(poly) = as_polygon(&g.geometry) else {
return Ok(None);
};
if n < 1 {
return Ok(None);
}
let Some(ring) = poly.interiors().get((n - 1) as usize) else {
return Ok(None);
};
out(
Geometry::LineString(ring.clone()),
g.srid,
"ST_InteriorRingN",
)
.map(Some)
}
pub fn st_num_interior_rings(bytes: &[u8]) -> Result<Option<i64>> {
let g = geom::decode_auto(bytes)?;
Ok(as_polygon(&g.geometry).map(|p| p.interiors().len() as i64))
}
pub fn st_nrings(bytes: &[u8]) -> Result<i64> {
let g = geom::decode_auto(bytes)?;
Ok(match &g.geometry {
Geometry::Polygon(p) => 1 + p.interiors().len() as i64,
Geometry::MultiPolygon(mp) => mp.iter().map(|p| 1 + p.interiors().len() as i64).sum(),
_ => 0,
})
}
pub fn st_boundary(bytes: &[u8]) -> Result<Vec<u8>> {
const FUNC: &str = "ST_Boundary";
let g = geom::decode_auto(bytes)?;
let boundary = match &g.geometry {
Geometry::Point(_) | Geometry::MultiPoint(_) => {
Geometry::Point(Point::new(f64::NAN, f64::NAN))
}
Geometry::LineString(line) => Geometry::MultiPoint(line_boundary(line)),
Geometry::MultiLineString(mls) => {
let mut ends: Vec<Coord<f64>> = Vec::new();
for line in mls {
for p in line_boundary(line).into_iter() {
let c = p.0;
if let Some(pos) = ends.iter().position(|e| *e == c) {
ends.remove(pos);
} else {
ends.push(c);
}
}
}
Geometry::MultiPoint(MultiPoint::new(ends.into_iter().map(Point::from).collect()))
}
Geometry::Polygon(poly) => rings_to_geometry(
std::iter::once(poly.exterior().clone())
.chain(poly.interiors().iter().cloned())
.collect(),
),
Geometry::MultiPolygon(mp) => rings_to_geometry(
mp.iter()
.flat_map(|poly| {
std::iter::once(poly.exterior().clone()).chain(poly.interiors().iter().cloned())
})
.collect(),
),
Geometry::Rect(_) | Geometry::Triangle(_) | Geometry::Line(_) => {
return Err(Error::Unsupported {
func: FUNC,
reason: "unsupported geometry type".into(),
});
}
Geometry::GeometryCollection(_) => {
return Err(Error::Unsupported {
func: FUNC,
reason: "GeometryCollection operands are not supported".into(),
});
}
};
out(boundary, g.srid, FUNC)
}
fn line_boundary(line: &LineString<f64>) -> MultiPoint<f64> {
let (Some(first), Some(last)) = (line.0.first(), line.0.last()) else {
return MultiPoint::new(vec![]);
};
if first == last {
return MultiPoint::new(vec![]);
}
MultiPoint::new(vec![Point::from(*first), Point::from(*last)])
}
fn rings_to_geometry(mut rings: Vec<LineString<f64>>) -> Geometry<f64> {
if rings.len() == 1 {
Geometry::LineString(rings.remove(0))
} else {
Geometry::MultiLineString(MultiLineString::new(rings))
}
}
pub fn st_is_closed(bytes: &[u8]) -> Result<bool> {
if let Some(closed) = crate::functions::surface::is_closed(bytes)? {
return Ok(closed);
}
let g = geom::decode_auto(bytes)?;
Ok(match &g.geometry {
Geometry::LineString(l) => is_closed_line(l),
Geometry::MultiLineString(mls) => mls.iter().all(is_closed_line),
Geometry::Polygon(_) | Geometry::MultiPolygon(_) => true,
_ => false,
})
}
fn is_closed_line(l: &LineString<f64>) -> bool {
match (l.0.first(), l.0.last()) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
pub fn st_is_ring(bytes: &[u8]) -> Result<bool> {
const FUNC: &str = "ST_IsRing";
let g = geom::decode_auto(bytes)?;
let Some(line) = as_line(&g.geometry) else {
return Err(Error::Unsupported {
func: FUNC,
reason: "ST_IsRing() should only be called on a linear feature".into(),
});
};
use geo::algorithm::Validation;
Ok(is_closed_line(line) && Polygon::new(line.clone(), vec![]).is_valid())
}
pub fn st_add_point(line: &[u8], point: &[u8], position: Option<i64>) -> Result<Option<Vec<u8>>> {
let (g, mut coords, p) = match line_and_point(line, point, "ST_AddPoint")? {
Some(v) => v,
None => return Ok(None),
};
let at = match position {
None | Some(-1) => coords.len(),
Some(n) if n >= 0 && (n as usize) <= coords.len() => n as usize,
Some(_) => return Ok(None),
};
coords.insert(at, p);
out(
Geometry::LineString(LineString::new(coords)),
g.srid,
"ST_AddPoint",
)
.map(Some)
}
pub fn st_set_point(line: &[u8], index: i64, point: &[u8]) -> Result<Option<Vec<u8>>> {
let (g, mut coords, p) = match line_and_point(line, point, "ST_SetPoint")? {
Some(v) => v,
None => return Ok(None),
};
let Some(at) = resolve_index(index, coords.len()) else {
return Ok(None);
};
coords[at] = p;
out(
Geometry::LineString(LineString::new(coords)),
g.srid,
"ST_SetPoint",
)
.map(Some)
}
pub fn st_remove_point(line: &[u8], index: i64) -> Result<Option<Vec<u8>>> {
let g = geom::decode_auto(line)?;
let Some(l) = as_line(&g.geometry) else {
return Ok(None);
};
let mut coords = l.0.clone();
let Some(at) = resolve_index(index, coords.len()) else {
return Ok(None);
};
coords.remove(at);
out(
Geometry::LineString(LineString::new(coords)),
g.srid,
"ST_RemovePoint",
)
.map(Some)
}
fn resolve_index(index: i64, len: usize) -> Option<usize> {
if index >= 0 && (index as usize) < len {
Some(index as usize)
} else {
None
}
}
type LineEdit = (Geom, Vec<Coord<f64>>, Coord<f64>);
fn line_and_point(line: &[u8], point: &[u8], func: &'static str) -> Result<Option<LineEdit>> {
let g = geom::decode_auto(line)?;
let p = geom::decode_auto(point)?;
if g.srid > 0 && p.srid > 0 && g.srid != p.srid {
return Err(Error::MixedSrid {
func,
a: g.srid,
b: p.srid,
});
}
let (Some(l), Geometry::Point(pt)) = (as_line(&g.geometry), &p.geometry) else {
return Ok(None);
};
let coords = l.0.clone();
let c = pt.0;
Ok(Some((g, coords, c)))
}
pub fn st_make_line(a: &[u8], b: &[u8]) -> Result<Vec<u8>> {
const FUNC: &str = "ST_MakeLine";
let ga = geom::decode_auto(a)?;
let gb = geom::decode_auto(b)?;
if ga.srid > 0 && gb.srid > 0 && ga.srid != gb.srid {
return Err(Error::MixedSrid {
func: FUNC,
a: ga.srid,
b: gb.srid,
});
}
let mut coords = Vec::new();
for g in [&ga.geometry, &gb.geometry] {
match g {
Geometry::Point(p) => coords.push(p.0),
Geometry::LineString(l) => coords.extend(l.0.iter().copied()),
Geometry::MultiPoint(mp) => coords.extend(mp.iter().map(|p| p.0)),
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "arguments must be points or linestrings".into(),
});
}
}
}
let srid = if ga.srid > 0 { ga.srid } else { gb.srid };
out(Geometry::LineString(LineString::new(coords)), srid, FUNC)
}
pub fn st_make_polygon(bytes: &[u8]) -> Result<Vec<u8>> {
const FUNC: &str = "ST_MakePolygon";
let g = geom::decode_auto(bytes)?;
let Some(line) = as_line(&g.geometry) else {
return Err(Error::Unsupported {
func: FUNC,
reason: "argument must be a LINESTRING".into(),
});
};
if !is_closed_line(line) {
return Err(Error::Unsupported {
func: FUNC,
reason: "shell is not closed".into(),
});
}
out(
Geometry::Polygon(Polygon::new(line.clone(), vec![])),
g.srid,
FUNC,
)
}
pub fn st_multi(bytes: &[u8]) -> Result<Vec<u8>> {
const FUNC: &str = "ST_Multi";
let g = geom::decode_auto(bytes)?;
let multi = match g.geometry {
Geometry::Point(p) => Geometry::MultiPoint(MultiPoint::new(vec![p])),
Geometry::LineString(l) => Geometry::MultiLineString(MultiLineString::new(vec![l])),
Geometry::Polygon(p) => Geometry::MultiPolygon(geo_types::MultiPolygon::new(vec![p])),
other @ (Geometry::MultiPoint(_)
| Geometry::MultiLineString(_)
| Geometry::MultiPolygon(_)) => other,
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "unsupported geometry type".into(),
});
}
};
out(multi, g.srid, FUNC)
}
pub fn st_snap_to_grid(bytes: &[u8], size_x: f64, size_y: f64) -> Result<Vec<u8>> {
const FUNC: &str = "ST_SnapToGrid";
let mut g = geom::decode_auto(bytes)?;
let snap = |v: f64, size: f64| {
if size > 0.0 {
(v / size).round() * size
} else {
v
}
};
map_coords(&mut g.geometry, &mut |c| Coord {
x: snap(c.x, size_x),
y: snap(c.y, size_y),
});
out(g.geometry, g.srid, FUNC)
}
pub fn st_flip_coordinates(bytes: &[u8]) -> Result<Vec<u8>> {
let mut g = geom::decode_auto(bytes)?;
map_coords(&mut g.geometry, &mut |c| Coord { x: c.y, y: c.x });
out(g.geometry, g.srid, "ST_FlipCoordinates")
}
pub fn st_shift_longitude(bytes: &[u8]) -> Result<Vec<u8>> {
let mut g = geom::decode_auto(bytes)?;
map_coords(&mut g.geometry, &mut |c| Coord {
x: if c.x < 0.0 { c.x + 360.0 } else { c.x },
y: c.y,
});
out(g.geometry, g.srid, "ST_ShiftLongitude")
}
pub fn st_expand(bytes: &[u8], units: f64) -> Result<Option<Vec<u8>>> {
const FUNC: &str = "ST_Expand";
let g = geom::decode_auto(bytes)?;
let Some(env) = geom::envelope(&g.geometry) else {
return Ok(None);
};
let (minx, miny) = (env.min_x - units, env.min_y - units);
let (maxx, maxy) = (env.max_x + units, env.max_y + units);
let ring = LineString::new(vec![
Coord { x: minx, y: miny },
Coord { x: minx, y: maxy },
Coord { x: maxx, y: maxy },
Coord { x: maxx, y: miny },
Coord { x: minx, y: miny },
]);
out(Geometry::Polygon(Polygon::new(ring, vec![])), g.srid, FUNC).map(Some)
}
pub(crate) fn map_coords_pub(g: &mut Geometry<f64>, f: &mut impl FnMut(Coord<f64>) -> Coord<f64>) {
map_coords(g, f)
}
fn map_coords(g: &mut Geometry<f64>, f: &mut impl FnMut(Coord<f64>) -> Coord<f64>) {
match g {
Geometry::Point(p) => p.0 = f(p.0),
Geometry::MultiPoint(mp) => {
for p in mp.iter_mut() {
p.0 = f(p.0);
}
}
Geometry::LineString(l) => map_line(l, f),
Geometry::MultiLineString(mls) => {
for l in mls.iter_mut() {
map_line(l, f);
}
}
Geometry::Polygon(p) => map_polygon(p, f),
Geometry::MultiPolygon(mp) => {
for p in mp.iter_mut() {
map_polygon(p, f);
}
}
Geometry::GeometryCollection(gc) => {
for g in gc.iter_mut() {
map_coords(g, f);
}
}
Geometry::Rect(_) | Geometry::Triangle(_) | Geometry::Line(_) => {}
}
}
fn map_line(l: &mut LineString<f64>, f: &mut impl FnMut(Coord<f64>) -> Coord<f64>) {
for c in l.0.iter_mut() {
*c = f(*c);
}
}
fn map_polygon(p: &mut Polygon<f64>, f: &mut impl FnMut(Coord<f64>) -> Coord<f64>) {
let mut exterior = p.exterior().clone();
map_line(&mut exterior, f);
let interiors: Vec<LineString<f64>> = p
.interiors()
.iter()
.map(|r| {
let mut r = r.clone();
map_line(&mut r, f);
r
})
.collect();
*p = Polygon::new(exterior, interiors);
}
pub fn st_force_polygon_cw(bytes: &[u8]) -> Result<Vec<u8>> {
orient(
bytes,
geo::algorithm::orient::Direction::Reversed,
"ST_ForcePolygonCW",
)
}
pub fn st_force_polygon_ccw(bytes: &[u8]) -> Result<Vec<u8>> {
orient(
bytes,
geo::algorithm::orient::Direction::Default,
"ST_ForcePolygonCCW",
)
}
fn orient(
bytes: &[u8],
direction: geo::algorithm::orient::Direction,
func: &'static str,
) -> Result<Vec<u8>> {
use geo::algorithm::Orient;
let g = geom::decode_auto(bytes)?;
let oriented = match g.geometry {
Geometry::Polygon(p) => Geometry::Polygon(p.orient(direction)),
Geometry::MultiPolygon(mp) => Geometry::MultiPolygon(mp.orient(direction)),
other => other,
};
out(oriented, g.srid, func)
}
pub fn st_is_polygon_cw(bytes: &[u8]) -> Result<bool> {
ring_orientation(bytes, true)
}
pub fn st_is_polygon_ccw(bytes: &[u8]) -> Result<bool> {
ring_orientation(bytes, false)
}
fn ring_orientation(bytes: &[u8], want_cw: bool) -> Result<bool> {
let g = geom::decode_auto(bytes)?;
fn check(p: &Polygon<f64>, want_cw: bool) -> bool {
let exterior_cw = signed_area(p.exterior()) < 0.0;
exterior_cw == want_cw
&& p.interiors()
.iter()
.all(|r| (signed_area(r) < 0.0) != want_cw)
}
Ok(match &g.geometry {
Geometry::Polygon(p) => check(p, want_cw),
Geometry::MultiPolygon(mp) => mp.iter().all(|p| check(p, want_cw)),
_ => true,
})
}
fn signed_area(ring: &LineString<f64>) -> f64 {
let mut sum = 0.0;
for line in ring.lines() {
sum += (line.end.x - line.start.x) * (line.end.y + line.start.y);
}
-sum / 2.0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::functions::io::{st_as_text, st_geom_from_text};
fn g(wkt: &str) -> Vec<u8> {
st_geom_from_text(wkt, None).unwrap()
}
fn wkt(blob: &[u8]) -> String {
st_as_text(blob).unwrap()
}
const HOLED: &str = "POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1))";
#[test]
fn ring_accessors_follow_postgis_indexing_and_null_rules() {
assert_eq!(
wkt(&st_exterior_ring(&g(HOLED)).unwrap().unwrap()),
"LINESTRING(0 0,4 0,4 4,0 4,0 0)"
);
assert_eq!(
wkt(&st_interior_ring_n(&g(HOLED), 1).unwrap().unwrap()),
"LINESTRING(1 1,2 1,2 2,1 2,1 1)"
);
assert!(st_interior_ring_n(&g(HOLED), 2).unwrap().is_none());
assert!(st_interior_ring_n(&g(HOLED), 0).unwrap().is_none());
assert!(
st_exterior_ring(&g("LINESTRING(0 0,1 1)"))
.unwrap()
.is_none()
);
assert!(st_num_interior_rings(&g("POINT(0 0)")).unwrap().is_none());
assert_eq!(st_num_interior_rings(&g(HOLED)).unwrap(), Some(1));
assert_eq!(st_nrings(&g(HOLED)).unwrap(), 2);
}
#[test]
fn boundary_shapes_match_postgis() {
assert_eq!(
wkt(&st_boundary(&g("POLYGON((0 0,4 0,4 4,0 4,0 0))")).unwrap()),
"LINESTRING(0 0,4 0,4 4,0 4,0 0)"
);
assert_eq!(
wkt(&st_boundary(&g(HOLED)).unwrap()),
"MULTILINESTRING((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1))"
);
assert_eq!(
wkt(&st_boundary(&g("LINESTRING(0 0,1 1,2 0)")).unwrap()),
"MULTIPOINT((0 0),(2 0))"
);
assert_eq!(
wkt(&st_boundary(&g("LINESTRING(0 0,1 1,1 0,0 0)")).unwrap()),
"MULTIPOINT EMPTY"
);
assert_eq!(wkt(&st_boundary(&g("POINT(1 1)")).unwrap()), "POINT EMPTY");
}
#[test]
fn closed_and_ring_predicates() {
assert!(st_is_closed(&g("POLYGON((0 0,4 0,4 4,0 4,0 0))")).unwrap());
assert!(!st_is_closed(&g("LINESTRING(0 0,1 1)")).unwrap());
assert!(st_is_closed(&g("LINESTRING(0 0,1 1,1 0,0 0)")).unwrap());
assert!(st_is_ring(&g("LINESTRING(0 0,1 1,1 0,0 0)")).unwrap());
assert!(!st_is_ring(&g("LINESTRING(0 0,1 1)")).unwrap());
assert!(st_is_ring(&g("POINT(0 0)")).is_err());
}
#[test]
fn vertex_surgery_is_zero_based() {
let line = g("LINESTRING(0 0,1 1)");
let p = g("POINT(9 9)");
assert_eq!(
wkt(&st_add_point(&line, &g("POINT(2 2)"), None)
.unwrap()
.unwrap()),
"LINESTRING(0 0,1 1,2 2)"
);
assert_eq!(
wkt(&st_add_point(&line, &p, Some(0)).unwrap().unwrap()),
"LINESTRING(9 9,0 0,1 1)"
);
assert_eq!(
wkt(&st_set_point(&line, 0, &p).unwrap().unwrap()),
"LINESTRING(9 9,1 1)"
);
assert_eq!(
wkt(&st_remove_point(&g("LINESTRING(0 0,1 1,2 2)"), 0)
.unwrap()
.unwrap()),
"LINESTRING(1 1,2 2)"
);
assert!(st_remove_point(&line, 5).unwrap().is_none());
assert!(st_set_point(&g("POINT(0 0)"), 0, &p).unwrap().is_none());
}
#[test]
fn constructors_and_coordinate_ops() {
assert_eq!(
wkt(&st_make_line(&g("POINT(0 0)"), &g("POINT(1 1)")).unwrap()),
"LINESTRING(0 0,1 1)"
);
assert_eq!(
wkt(&st_make_polygon(&g("LINESTRING(0 0,1 0,1 1,0 0)")).unwrap()),
"POLYGON((0 0,1 0,1 1,0 0))"
);
assert!(st_make_polygon(&g("LINESTRING(0 0,1 0)")).is_err());
assert_eq!(
wkt(&st_multi(&g("POINT(1 2)")).unwrap()),
"MULTIPOINT((1 2))"
);
assert_eq!(
wkt(&st_snap_to_grid(&g("POINT(1.23 4.57)"), 0.5, 0.5).unwrap()),
"POINT(1 4.5)"
);
let snapped = st_snap_to_grid(&g("POINT(1.23 4.57)"), 0.1, 1.0).unwrap();
assert_eq!(
crate::functions::accessors::st_x(&snapped).unwrap(),
Some((1.23f64 / 0.1).round() * 0.1)
);
assert!(wkt(&snapped).starts_with("POINT(1.2"));
assert_eq!(
wkt(&st_flip_coordinates(&g("POINT(1 2)")).unwrap()),
"POINT(2 1)"
);
assert_eq!(
wkt(&st_shift_longitude(&g("POINT(-10 5)")).unwrap()),
"POINT(350 5)"
);
assert_eq!(
wkt(&st_expand(&g("POINT(1 1)"), 2.0).unwrap().unwrap()),
"POLYGON((-1 -1,-1 3,3 3,3 -1,-1 -1))"
);
}
#[test]
fn snap_to_grid_reaches_inside_polygon_rings() {
let snapped = st_snap_to_grid(
&g("POLYGON((0.1 0.1,4.4 0.1,4.4 4.4,0.1 4.4,0.1 0.1))"),
1.0,
1.0,
)
.unwrap();
assert_eq!(wkt(&snapped), "POLYGON((0 0,4 0,4 4,0 4,0 0))");
}
}