use geo::algorithm::line_intersection::{LineIntersection, line_intersection};
#[cfg(feature = "overlay")]
use geo_types::MultiPolygon;
use geo_types::{Coord, Geometry, Line, LineString, MultiLineString, 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 key(c: Coord<f64>) -> (u64, u64) {
(c.x.to_bits(), c.y.to_bits())
}
fn collapse(coords: &[Coord<f64>]) -> Vec<Coord<f64>> {
let mut out: Vec<Coord<f64>> = Vec::with_capacity(coords.len());
for &c in coords {
if out.last().map(|&p| key(p) != key(c)).unwrap_or(true) {
out.push(c);
}
}
out
}
pub fn st_is_simple(bytes: &[u8]) -> Result<bool> {
const FUNC: &str = "ST_IsSimple";
let g = geom::decode_auto(bytes)?;
Ok(match &g.geometry {
Geometry::Point(_) => true,
Geometry::MultiPoint(mp) => {
let mut seen = std::collections::HashSet::new();
mp.0.iter().all(|p| seen.insert(key(p.0)))
}
Geometry::Line(_) => true,
Geometry::LineString(ls) => lineal_is_simple(&[ls.0.as_slice()]),
Geometry::MultiLineString(mls) => {
lineal_is_simple(&mls.0.iter().map(|l| l.0.as_slice()).collect::<Vec<_>>())
}
Geometry::Polygon(p) => rings_are_simple(p),
Geometry::MultiPolygon(mp) => mp.0.iter().all(rings_are_simple),
Geometry::Rect(_) | Geometry::Triangle(_) => true,
Geometry::GeometryCollection(_) => {
return Err(Error::Unsupported {
func: FUNC,
reason: "GeometryCollection is not supported".into(),
});
}
})
}
fn rings_are_simple(p: &Polygon<f64>) -> bool {
std::iter::once(p.exterior())
.chain(p.interiors())
.all(|r| lineal_is_simple(&[r.0.as_slice()]))
}
fn lineal_is_simple(parts: &[&[Coord<f64>]]) -> bool {
let parts: Vec<Vec<Coord<f64>>> = parts
.iter()
.map(|p| collapse(p))
.filter(|p| p.len() >= 2)
.collect();
if parts.is_empty() {
return true; }
let mut interior_seen = std::collections::HashSet::new();
let mut count: std::collections::HashMap<(u64, u64), usize> = std::collections::HashMap::new();
for part in &parts {
for (i, &c) in part.iter().enumerate() {
*count.entry(key(c)).or_default() += 1;
if i != 0 && i != part.len() - 1 {
interior_seen.insert(key(c));
}
}
}
if count
.iter()
.any(|(k, &n)| n > 1 && interior_seen.contains(k))
{
return false;
}
let segments: Vec<Line<f64>> = parts
.iter()
.flat_map(|p| p.windows(2).map(|w| Line::new(w[0], w[1])))
.collect();
for (i, a) in segments.iter().enumerate() {
for b in &segments[i + 1..] {
match line_intersection(*a, *b) {
None => {}
Some(LineIntersection::Collinear { .. }) => return false,
Some(LineIntersection::SinglePoint { intersection, .. }) => {
let ends = |l: &Line<f64>| {
key(l.start) == key(intersection) || key(l.end) == key(intersection)
};
if !(ends(a) && ends(b)) {
return false;
}
}
}
}
}
true
}
pub fn st_line_merge(bytes: &[u8]) -> Result<Vec<u8>> {
line_merge(bytes, false)
}
pub fn st_line_merge_directed(bytes: &[u8], directed: bool) -> Result<Vec<u8>> {
line_merge(bytes, directed)
}
fn line_merge(bytes: &[u8], directed: bool) -> Result<Vec<u8>> {
const FUNC: &str = "ST_LineMerge";
let g = geom::decode_auto(bytes)?;
let parts: Vec<Vec<Coord<f64>>> = match &g.geometry {
Geometry::LineString(ls) => vec![ls.0.clone()],
Geometry::MultiLineString(mls) => mls.0.iter().map(|l| l.0.clone()).collect(),
Geometry::Line(l) => vec![vec![l.start, l.end]],
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "argument must be a LINESTRING or MULTILINESTRING (PostGIS answers \
GEOMETRYCOLLECTION EMPTY here; kenro will not return a collection, \
and an empty result would hide the mistake)"
.into(),
});
}
};
let merged = merge_chains(parts, directed);
let geometry = match merged.len() {
1 => Geometry::LineString(LineString::new(merged.into_iter().next().unwrap())),
_ => Geometry::MultiLineString(MultiLineString::new(
merged.into_iter().map(LineString::new).collect(),
)),
};
out(geometry, g.srid, FUNC)
}
fn merge_chains(parts: Vec<Vec<Coord<f64>>>, directed: bool) -> Vec<Vec<Coord<f64>>> {
let mut parts: Vec<Vec<Coord<f64>>> = parts
.into_iter()
.map(|p| collapse(&p))
.filter(|p| p.len() >= 2)
.collect();
if parts.len() < 2 {
return parts;
}
let mut degree: std::collections::HashMap<(u64, u64), usize> = std::collections::HashMap::new();
for p in &parts {
*degree.entry(key(p[0])).or_default() += 1;
*degree.entry(key(*p.last().unwrap())).or_default() += 1;
}
let mut used = vec![false; parts.len()];
let mut result = Vec::new();
for i in 0..parts.len() {
if used[i] {
continue;
}
used[i] = true;
let mut chain = std::mem::take(&mut parts[i]);
loop {
let mut grew = false;
for side in [true, false] {
let node = if side {
*chain.last().unwrap()
} else {
chain[0]
};
if degree.get(&key(node)).copied().unwrap_or(0) != 2 {
continue;
}
let Some(j) = (0..parts.len()).find(|&j| {
!used[j] && {
let p = &parts[j];
let head = key(p[0]) == key(node);
let tail = key(*p.last().unwrap()) == key(node);
if directed {
if side { head } else { tail }
} else {
head || tail
}
}
}) else {
continue;
};
if key(chain[0]) == key(*chain.last().unwrap()) {
continue;
}
used[j] = true;
let mut piece = std::mem::take(&mut parts[j]);
if side {
if key(piece[0]) != key(node) {
piece.reverse();
}
chain.extend_from_slice(&piece[1..]);
} else {
if key(*piece.last().unwrap()) != key(node) {
piece.reverse();
}
piece.pop();
piece.extend_from_slice(&chain);
chain = piece;
}
grew = true;
}
if !grew {
break;
}
}
result.push(chain);
}
result
}
#[cfg(feature = "overlay")]
pub fn st_split(input: &[u8], blade: &[u8]) -> Result<Vec<u8>> {
const FUNC: &str = "ST_Split";
let g = geom::decode_auto(input)?;
let b = geom::decode_auto(blade)?;
if g.srid > 0 && b.srid > 0 && g.srid != b.srid {
return Err(Error::MixedSrid {
func: FUNC,
a: g.srid,
b: b.srid,
});
}
let geometry = match &g.geometry {
Geometry::LineString(_) | Geometry::MultiLineString(_) | Geometry::Line(_) => {
let parts = lineal_parts(FUNC, &g.geometry)?;
let cuts = cut_points(FUNC, &b.geometry)?;
Geometry::MultiLineString(MultiLineString::new(
split_lines(parts, &cuts)
.into_iter()
.map(LineString::new)
.collect(),
))
}
Geometry::Polygon(_)
| Geometry::MultiPolygon(_)
| Geometry::Rect(_)
| Geometry::Triangle(_) => {
Geometry::MultiPolygon(slice_areal(FUNC, &g.geometry, &b.geometry)?)
}
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "input must be lineal or areal".into(),
});
}
};
out(geometry, g.srid, FUNC)
}
#[cfg(feature = "overlay")]
fn lineal_parts(func: &'static str, g: &Geometry<f64>) -> Result<Vec<Vec<Coord<f64>>>> {
Ok(match g {
Geometry::LineString(ls) => vec![collapse(&ls.0)],
Geometry::MultiLineString(mls) => mls.0.iter().map(|l| collapse(&l.0)).collect(),
Geometry::Line(l) => vec![vec![l.start, l.end]],
_ => {
return Err(Error::Unsupported {
func,
reason: "input must be lineal or areal".into(),
});
}
})
}
#[cfg(feature = "overlay")]
enum Blade {
Points(Vec<Coord<f64>>),
Lines(Vec<Line<f64>>),
}
#[cfg(feature = "overlay")]
fn cut_points(func: &'static str, blade: &Geometry<f64>) -> Result<Blade> {
Ok(match blade {
Geometry::Point(p) => Blade::Points(vec![p.0]),
Geometry::MultiPoint(mp) => Blade::Points(mp.0.iter().map(|p| p.0).collect()),
Geometry::LineString(ls) => Blade::Lines(ls.lines().collect()),
Geometry::MultiLineString(mls) => {
Blade::Lines(mls.iter().flat_map(|l| l.lines()).collect())
}
Geometry::Line(l) => Blade::Lines(vec![*l]),
Geometry::Polygon(_) | Geometry::MultiPolygon(_) => {
return Err(Error::Unsupported {
func,
reason: "a line can only be split by a point or a line".into(),
});
}
_ => {
return Err(Error::Unsupported {
func,
reason: "unsupported blade".into(),
});
}
})
}
#[cfg(feature = "overlay")]
fn split_lines(parts: Vec<Vec<Coord<f64>>>, blade: &Blade) -> Vec<Vec<Coord<f64>>> {
let mut out = Vec::new();
for part in parts {
if part.len() < 2 {
continue;
}
let mut current = vec![part[0]];
for w in part.windows(2) {
let seg = Line::new(w[0], w[1]);
let mut hits: Vec<Coord<f64>> = match blade {
Blade::Points(ps) => ps.iter().copied().filter(|&p| on_segment(seg, p)).collect(),
Blade::Lines(ls) => ls
.iter()
.filter_map(|&b| match line_intersection(seg, b) {
Some(LineIntersection::SinglePoint { intersection, .. }) => {
Some(intersection)
}
_ => None,
})
.collect(),
};
hits.retain(|&p| key(p) != key(seg.start) && key(p) != key(seg.end));
hits.sort_by(|a, b| {
let d = |c: &Coord<f64>| (c.x - seg.start.x).powi(2) + (c.y - seg.start.y).powi(2);
d(a).partial_cmp(&d(b)).unwrap_or(std::cmp::Ordering::Equal)
});
hits.dedup_by_key(|p| key(*p));
for h in hits {
current.push(h);
out.push(std::mem::take(&mut current));
current = vec![h];
}
current.push(w[1]);
let ends_at_cut = match blade {
Blade::Points(ps) => ps.iter().any(|&p| key(p) == key(w[1])),
Blade::Lines(ls) => ls.iter().any(|&b| {
matches!(
line_intersection(seg, b),
Some(LineIntersection::SinglePoint { intersection, .. }) if key(intersection) == key(w[1])
)
}),
};
if ends_at_cut && key(w[1]) != key(*part.last().unwrap()) {
out.push(std::mem::take(&mut current));
current = vec![w[1]];
}
}
if current.len() >= 2 {
out.push(current);
}
}
out
}
#[cfg(feature = "overlay")]
fn on_segment(seg: Line<f64>, p: Coord<f64>) -> bool {
use geo::algorithm::{Kernel, kernels::RobustKernel};
if RobustKernel::orient2d(seg.start, seg.end, p) != geo::algorithm::Orientation::Collinear {
return false;
}
let within = |a: f64, b: f64, v: f64| v >= a.min(b) && v <= a.max(b);
within(seg.start.x, seg.end.x, p.x) && within(seg.start.y, seg.end.y, p.y)
}
#[cfg(feature = "overlay")]
fn slice_areal(
func: &'static str,
input: &Geometry<f64>,
blade: &Geometry<f64>,
) -> Result<MultiPolygon<f64>> {
use i_overlay::core::fill_rule::FillRule;
use i_overlay::float::slice::FloatSlice;
let polygons: Vec<Polygon<f64>> = match input {
Geometry::Polygon(p) => vec![p.clone()],
Geometry::MultiPolygon(mp) => mp.0.clone(),
Geometry::Rect(r) => vec![r.to_polygon()],
Geometry::Triangle(t) => vec![t.to_polygon()],
_ => unreachable!("caller checked the class"),
};
let cutter: Vec<Vec<[f64; 2]>> = match blade {
Geometry::LineString(ls) => vec![path(&ls.0)],
Geometry::MultiLineString(mls) => mls.0.iter().map(|l| path(&l.0)).collect(),
Geometry::Line(l) => vec![vec![[l.start.x, l.start.y], [l.end.x, l.end.y]]],
Geometry::Point(_) | Geometry::MultiPoint(_) => {
return Err(Error::Unsupported {
func,
reason: "splitting a polygon by a point is unsupported (PostGIS refuses this too)"
.into(),
});
}
_ => {
return Err(Error::Unsupported {
func,
reason: "a polygon can only be split by a line".into(),
});
}
};
use geo::algorithm::orient::{Direction, Orient};
let shapes: Vec<Vec<Vec<[f64; 2]>>> = polygons
.iter()
.map(|p| {
let p = p.orient(Direction::Default);
std::iter::once(path(&p.exterior().0))
.chain(p.interiors().iter().map(|r| path(&r.0)))
.collect()
})
.collect();
let sliced = shapes.slice_by(&cutter, FillRule::NonZero);
Ok(MultiPolygon::new(
sliced
.into_iter()
.filter(|shape| !shape.is_empty())
.map(|shape| {
let mut contours = shape.into_iter().map(ring);
let exterior = contours.next().unwrap_or_else(|| LineString::new(vec![]));
Polygon::new(exterior, contours.collect())
})
.collect(),
))
}
#[cfg(feature = "overlay")]
fn path(coords: &[Coord<f64>]) -> Vec<[f64; 2]> {
let mut v: Vec<[f64; 2]> = coords.iter().map(|c| [c.x, c.y]).collect();
if v.len() > 1 && v.first() == v.last() {
v.pop();
}
v
}
#[cfg(feature = "overlay")]
fn ring(contour: Vec<[f64; 2]>) -> LineString<f64> {
let mut coords: Vec<Coord<f64>> = contour
.into_iter()
.map(|p| Coord { x: p[0], y: p[1] })
.collect();
if let Some(&first) = coords.first() {
coords.push(first);
}
LineString::new(coords)
}
#[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 simple(wkt: &str) -> bool {
st_is_simple(&g(wkt)).unwrap()
}
#[test]
fn simplicity_matches_postgis_on_the_cases_that_distinguish_the_rules() {
assert!(simple("LINESTRING(0 0,10 10)"));
assert!(!simple("LINESTRING(0 0,10 10,0 10,10 0)"));
assert!(simple("LINESTRING(0 0,10 0,10 10,0 10,0 0)"));
assert!(!simple("LINESTRING(0 0,10 0,10 10,0 10,0 0,5 5)"));
assert!(simple("LINESTRING(0 0,10 0,10 10,0 0)"));
assert!(!simple("LINESTRING(0 0,5 0,5 5,5 0,10 0)"));
assert!(!simple("LINESTRING(0 0,10 0,5 0)"));
assert!(!simple("LINESTRING(0 0,1 1,0 0)"));
assert!(!simple("LINESTRING(0 0,10 0,5 5,5 0)"));
assert!(simple("LINESTRING(0 0,0 0,1 1)"));
assert!(simple("LINESTRING EMPTY"));
}
#[test]
fn simplicity_across_components_and_dimensions() {
assert!(simple("MULTILINESTRING((0 0,10 10),(10 10,20 20))"));
assert!(simple("MULTILINESTRING((0 0,1 1),(1 1,2 0),(1 1,0 2))"));
assert!(!simple("MULTILINESTRING((0 0,10 10),(0 10,10 0))"));
assert!(!simple("MULTILINESTRING((0 0,10 0),(5 0,15 0))"));
assert!(!simple("MULTILINESTRING((0 0,5 5,10 10),(5 5,10 0))"));
assert!(!simple("MULTILINESTRING((0 0,1 1),(1 1,0 0))"));
assert!(simple("MULTIPOINT(0 0,1 1)"));
assert!(!simple("MULTIPOINT(0 0,0 0)"));
assert!(simple("POINT(0 0)"));
assert!(simple("POLYGON((0 0,10 0,10 10,0 10,0 0))"));
assert!(!simple("POLYGON((0 0,10 10,10 0,0 10,0 0))"));
assert!(simple(
"POLYGON((0 0,10 0,10 10,0 10,0 0),(0 0,5 2,5 5,0 0))"
));
assert!(!simple(
"POLYGON((0 0,10 0,10 10,0 10,0 0),(2 2,8 2,2 8,8 8,2 2))"
));
assert!(simple(
"MULTIPOLYGON(((0 0,1 0,1 1,0 0)),((0 0,1 0,1 1,0 0)))"
));
}
fn merged(wkt: &str) -> String {
st_as_text(&st_line_merge(&g(wkt)).unwrap()).unwrap()
}
#[test]
fn merging_joins_only_where_exactly_two_ends_meet() {
assert_eq!(
merged("MULTILINESTRING((0 0,1 1),(1 1,2 2))"),
"LINESTRING(0 0,1 1,2 2)"
);
for wkt in [
"MULTILINESTRING((1 1,0 0),(1 1,2 2))",
"MULTILINESTRING((2 2,1 1),(0 0,1 1))",
] {
assert!(
matches!(
merged(wkt).as_str(),
"LINESTRING(0 0,1 1,2 2)" | "LINESTRING(2 2,1 1,0 0)"
),
"{wkt} -> {}",
merged(wkt)
);
}
assert_eq!(merged("LINESTRING(0 0,1 1)"), "LINESTRING(0 0,1 1)");
let ring = merged("MULTILINESTRING((0 0,1 1),(1 1,2 2),(2 2,0 0))");
assert!(ring.starts_with("LINESTRING("), "{ring}");
let pts: Vec<&str> = ring
.trim_start_matches("LINESTRING(")
.trim_end_matches(')')
.split(',')
.collect();
assert_eq!(pts.len(), 4, "{ring}");
assert_eq!(pts[0], pts[3], "{ring}");
let mut sorted = pts[..3].to_vec();
sorted.sort_unstable();
assert_eq!(sorted, ["0 0", "1 1", "2 2"], "{ring}");
assert_eq!(
merged("MULTILINESTRING((0 0,1 1),(2 2,3 3))"),
"MULTILINESTRING((0 0,1 1),(2 2,3 3))"
);
let y = merged("MULTILINESTRING((0 0,1 1),(1 1,2 0),(1 1,0 2))");
assert!(y.starts_with("MULTILINESTRING("), "{y}");
assert_eq!(y.matches("),(").count(), 2, "{y}");
assert_eq!(
merged("MULTILINESTRING((0 0,2 2),(1 1,3 3))"),
"MULTILINESTRING((0 0,2 2),(1 1,3 3))"
);
assert!(st_line_merge(&g("POLYGON((0 0,1 0,1 1,0 0))")).is_err());
}
#[test]
fn directed_merging_honours_the_original_directions() {
let d = |wkt: &str| st_as_text(&st_line_merge_directed(&g(wkt), true).unwrap()).unwrap();
assert_eq!(
d("MULTILINESTRING((0 0,1 1),(1 1,2 2))"),
"LINESTRING(0 0,1 1,2 2)"
);
assert_eq!(
d("MULTILINESTRING((0 0,1 1),(2 2,1 1))"),
"MULTILINESTRING((0 0,1 1),(2 2,1 1))"
);
}
#[cfg(feature = "overlay")]
#[test]
fn splitting_a_line_yields_the_pieces_in_order() {
let split = |a: &str, b: &str| st_as_text(&st_split(&g(a), &g(b)).unwrap()).unwrap();
assert_eq!(
split("LINESTRING(0 0,10 0)", "POINT(5 0)"),
"MULTILINESTRING((0 0,5 0),(5 0,10 0))"
);
assert_eq!(
split("LINESTRING(0 0,10 0)", "MULTIPOINT(3 0,7 0)"),
"MULTILINESTRING((0 0,3 0),(3 0,7 0),(7 0,10 0))"
);
assert_eq!(
split("LINESTRING(0 0,10 10)", "LINESTRING(0 10,10 0)"),
"MULTILINESTRING((0 0,5 5),(5 5,10 10))"
);
assert_eq!(
split(
"MULTILINESTRING((0 0,10 0),(0 5,10 5))",
"LINESTRING(5 -1,5 6)"
),
"MULTILINESTRING((0 0,5 0),(5 0,10 0),(0 5,5 5),(5 5,10 5))"
);
assert_eq!(
split("LINESTRING(0 0,10 0)", "POINT(5 5)"),
"MULTILINESTRING((0 0,10 0))"
);
assert_eq!(
split("LINESTRING(0 0,5 0,10 0)", "POINT(5 0)"),
"MULTILINESTRING((0 0,5 0),(5 0,10 0))"
);
}
#[cfg(feature = "overlay")]
#[test]
fn splitting_a_polygon_preserves_area_and_holes() {
use crate::functions::accessors::st_area;
let square = g("POLYGON((0 0,10 0,10 10,0 10,0 0))");
let cut = st_split(&square, &g("LINESTRING(5 -1,5 11)")).unwrap();
assert_eq!(
crate::functions::accessors::st_num_geometries(&cut).unwrap(),
2
);
assert!((st_area(&cut).unwrap() - 100.0).abs() < 1e-9);
let quartered = st_split(&square, &g("MULTILINESTRING((5 -1,5 11),(-1 5,11 5))")).unwrap();
assert_eq!(
crate::functions::accessors::st_num_geometries(&quartered).unwrap(),
4
);
assert!((st_area(&quartered).unwrap() - 100.0).abs() < 1e-9);
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 cut = st_split(&holed, &g("LINESTRING(5 -1,5 11)")).unwrap();
assert!((st_area(&cut).unwrap() - 96.0).abs() < 1e-9);
let uncut = st_split(&square, &g("LINESTRING(-1 -1,-1 5)")).unwrap();
assert_eq!(
crate::functions::accessors::st_num_geometries(&uncut).unwrap(),
1
);
assert!((st_area(&uncut).unwrap() - 100.0).abs() < 1e-9);
assert!(st_split(&square, &g("POINT(5 5)")).is_err());
}
}