use geo_types::{Coord, Geometry, Polygon};
use crate::error::{Error, Result};
use crate::functions::num;
use crate::geom;
const FUNC: &str = "ST_AsSVG";
pub fn st_as_svg(bytes: &[u8], rel: Option<i64>, digits: Option<i64>) -> Result<String> {
let g = geom::decode_auto(bytes)?;
if g.has_zm {
return Err(Error::Unsupported {
func: FUNC,
reason: "3D/M output is not supported in kenro; use ST_Force2D".into(),
});
}
let relative = rel.unwrap_or(0) != 0;
let digits = digits.unwrap_or(15).clamp(0, 15) as usize;
let mut out = String::new();
write_geometry(&mut out, &g.geometry, relative, digits)?;
Ok(out)
}
fn write_geometry(out: &mut String, g: &Geometry<f64>, rel: bool, d: usize) -> Result<()> {
match g {
Geometry::Point(p) => write_point(out, p.0, rel, d),
Geometry::MultiPoint(mp) => {
for (i, p) in mp.0.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_point(out, p.0, rel, d);
}
}
Geometry::Line(l) => write_path(out, &[l.start, l.end], false, rel, d),
Geometry::LineString(l) => write_path(out, &l.0, false, rel, d),
Geometry::MultiLineString(mls) => {
for (i, l) in mls.0.iter().enumerate() {
if i > 0 {
out.push(' ');
}
write_path(out, &l.0, false, rel, d);
}
}
Geometry::Polygon(p) => write_polygon(out, p, rel, d),
Geometry::Rect(r) => write_polygon(out, &r.to_polygon(), rel, d),
Geometry::Triangle(t) => write_polygon(out, &t.to_polygon(), rel, d),
Geometry::MultiPolygon(mp) => {
for (i, p) in mp.0.iter().enumerate() {
if i > 0 {
out.push(' ');
}
write_polygon(out, p, rel, d);
}
}
Geometry::GeometryCollection(_) => {
return Err(Error::Unsupported {
func: FUNC,
reason: "GeometryCollection is not supported".into(),
});
}
}
Ok(())
}
fn write_point(out: &mut String, c: Coord<f64>, rel: bool, d: usize) {
if !c.x.is_finite() || !c.y.is_finite() {
return;
}
let (nx, ny) = if rel { ("x", "y") } else { ("cx", "cy") };
out.push_str(&format!(
"{nx}=\"{}\" {ny}=\"{}\"",
num(c.x, d),
num(-c.y, d)
));
}
fn write_polygon(out: &mut String, p: &Polygon<f64>, rel: bool, d: usize) {
if p.exterior().0.is_empty() {
return;
}
write_path(out, &p.exterior().0, true, rel, d);
for ring in p.interiors() {
out.push(' ');
write_path(out, &ring.0, true, rel, d);
}
}
fn write_path(out: &mut String, coords: &[Coord<f64>], ring: bool, rel: bool, d: usize) {
let coords = if ring && coords.len() > 1 && coords.first() == coords.last() {
&coords[..coords.len() - 1]
} else {
coords
};
let Some(&first) = coords.first() else {
return;
};
out.push_str(&format!("M {} {}", num(first.x, d), num(-first.y, d)));
if coords.len() > 1 {
out.push_str(if rel { " l" } else { " L" });
let mut prev = first;
for &c in &coords[1..] {
let (x, y) = if rel {
(c.x - prev.x, -(c.y - prev.y))
} else {
(c.x, -c.y)
};
out.push_str(&format!(" {} {}", num(x, d), num(y, d)));
prev = c;
}
}
if ring {
out.push_str(if rel { " z" } else { " Z" });
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::functions::io::st_geom_from_text;
fn g(wkt: &str) -> Vec<u8> {
st_geom_from_text(wkt, None).unwrap()
}
fn svg(wkt: &str) -> String {
st_as_svg(&g(wkt), None, None).unwrap()
}
#[test]
fn absolute_output_is_byte_identical_to_postgis() {
assert_eq!(svg("POINT(1 2)"), r#"cx="1" cy="-2""#);
assert_eq!(
svg("POINT(1.23456789 2.3456789)"),
r#"cx="1.23456789" cy="-2.3456789""#
);
assert_eq!(svg("POINT(-1.5 -2.5)"), r#"cx="-1.5" cy="2.5""#);
assert_eq!(svg("LINESTRING(0 0,1 1,2 0)"), "M 0 0 L 1 -1 2 0");
assert_eq!(
svg("POLYGON((0 0,4 0,4 4,0 4,0 0))"),
"M 0 0 L 4 0 4 -4 0 -4 Z"
);
assert_eq!(
svg("POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1))"),
"M 0 0 L 4 0 4 -4 0 -4 Z M 1 -1 L 2 -1 2 -2 1 -2 Z"
);
assert_eq!(
svg("MULTIPOINT(0 0,1 1)"),
r#"cx="0" cy="0",cx="1" cy="-1""#
);
assert_eq!(
st_as_svg(&g("POINT(-0.001 0.001)"), Some(0), Some(2)).unwrap(),
r#"cx="0" cy="0""#
);
assert_eq!(
st_as_svg(&g("LINESTRING(0 0,0 0.001)"), Some(1), Some(2)).unwrap(),
"M 0 0 l 0 0"
);
assert_eq!(
svg("MULTILINESTRING((0 0,1 1),(2 2,3 3))"),
"M 0 0 L 1 -1 M 2 -2 L 3 -3"
);
assert_eq!(
svg("MULTIPOLYGON(((0 0,1 0,1 1,0 0)),((5 5,6 5,6 6,5 5)))"),
"M 0 0 L 1 0 1 -1 Z M 5 -5 L 6 -5 6 -6 Z"
);
assert_eq!(svg("LINESTRING EMPTY"), "");
assert_eq!(svg("POLYGON EMPTY"), "");
}
#[test]
fn relative_output_switches_the_commands_and_the_point_attributes() {
let rel = |wkt: &str| st_as_svg(&g(wkt), Some(1), None).unwrap();
assert_eq!(rel("POINT(1 2)"), r#"x="1" y="-2""#);
assert_eq!(rel("MULTIPOINT(0 0,1 1)"), r#"x="0" y="0",x="1" y="-1""#);
assert_eq!(rel("LINESTRING(0 0,1 1,2 0)"), "M 0 0 l 1 -1 1 1");
assert_eq!(
rel("POLYGON((0 0,4 0,4 4,0 4,0 0))"),
"M 0 0 l 4 0 0 -4 -4 0 z"
);
assert_eq!(
rel("POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1))"),
"M 0 0 l 4 0 0 -4 -4 0 z M 1 -1 l 1 0 0 -1 -1 0 z"
);
assert_eq!(
rel("MULTILINESTRING((0 0,1 1),(2 2,3 3))"),
"M 0 0 l 1 -1 M 2 -2 l 1 -1"
);
}
#[test]
fn precision_matches_postgis_including_the_relative_rounding() {
assert_eq!(
st_as_svg(&g("POINT(1.23456789 2.3456789)"), Some(0), Some(3)).unwrap(),
r#"cx="1.235" cy="-2.346""#
);
assert_eq!(
st_as_svg(
&g("LINESTRING(0.123456 0.7654321,1.111111 1.999999)"),
Some(0),
Some(2)
)
.unwrap(),
"M 0.12 -0.77 L 1.11 -2"
);
assert_eq!(
st_as_svg(
&g("LINESTRING(0.123456 0.7654321,1.111111 1.999999)"),
Some(1),
Some(2)
)
.unwrap(),
"M 0.12 -0.77 l 0.99 -1.23"
);
assert_eq!(
st_as_svg(&g("POINT(1.5 2.5)"), Some(0), Some(0)).unwrap(),
r#"cx="2" cy="-2""#
);
assert_eq!(
st_as_svg(&g("POINT(0.0000001 0.0000001)"), Some(0), Some(15)).unwrap(),
r#"cx="0.0000001" cy="-0.0000001""#
);
assert_eq!(
st_as_svg(&g("POINT(1000000 2000000)"), None, None).unwrap(),
r#"cx="1000000" cy="-2000000""#
);
}
}