use geo_types::{Geometry, LineString, Polygon};
use crate::error::{Error, Result};
use crate::functions::num;
use crate::geom;
const FUNC: &str = "ST_AsKML";
pub fn st_as_kml(bytes: &[u8], digits: Option<i64>, prefix: Option<&str>) -> Result<String> {
let mut 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 prefix = match prefix {
None | Some("") => String::new(),
Some(p) => {
if !is_xml_name(p) {
return Err(Error::Unsupported {
func: FUNC,
reason: format!(
"namespace prefix {p:?} is not an XML name; it is written into the \
output verbatim, so it must be one"
),
});
}
format!("{p}:")
}
};
let digits = digits.unwrap_or(15).clamp(0, 15) as usize;
if g.srid != 4326 {
crate::functions::transform::decoded::st_transform_in_place(&mut g, 4326).map_err(|e| {
match e {
Error::Unsupported { reason, .. } => Error::Unsupported { func: FUNC, reason },
other => other,
}
})?;
}
let mut out = String::new();
write_geometry(&mut out, &g.geometry, digits, &prefix)?;
Ok(out)
}
fn write_geometry(out: &mut String, g: &Geometry<f64>, d: usize, p: &str) -> Result<()> {
match g {
Geometry::Point(pt) => {
if !pt.0.x.is_finite() || !pt.0.y.is_finite() {
return Ok(()); }
out.push_str(&format!(
"<{p}Point><{p}coordinates>{},{}</{p}coordinates></{p}Point>",
num(pt.0.x, d),
num(pt.0.y, d)
));
}
Geometry::Line(l) => write_line(out, &LineString::new(vec![l.start, l.end]), d, p),
Geometry::LineString(l) => write_line(out, l, d, p),
Geometry::Polygon(poly) => write_polygon(out, poly, d, p),
Geometry::Rect(r) => write_polygon(out, &r.to_polygon(), d, p),
Geometry::Triangle(t) => write_polygon(out, &t.to_polygon(), d, p),
Geometry::MultiPoint(mp) => {
if mp.0.is_empty() {
return Ok(());
}
out.push_str(&format!("<{p}MultiGeometry>"));
for pt in &mp.0 {
write_geometry(out, &Geometry::Point(*pt), d, p)?;
}
out.push_str(&format!("</{p}MultiGeometry>"));
}
Geometry::MultiLineString(mls) => {
if mls.0.is_empty() {
return Ok(());
}
out.push_str(&format!("<{p}MultiGeometry>"));
for l in &mls.0 {
write_line(out, l, d, p);
}
out.push_str(&format!("</{p}MultiGeometry>"));
}
Geometry::MultiPolygon(mp) => {
if mp.0.is_empty() {
return Ok(());
}
out.push_str(&format!("<{p}MultiGeometry>"));
for poly in &mp.0 {
write_polygon(out, poly, d, p);
}
out.push_str(&format!("</{p}MultiGeometry>"));
}
Geometry::GeometryCollection(_) => {
return Err(Error::Unsupported {
func: FUNC,
reason: "GeometryCollection is not supported (PostGIS refuses this too)".into(),
});
}
}
Ok(())
}
fn write_line(out: &mut String, l: &LineString<f64>, d: usize, p: &str) {
if l.0.is_empty() {
return;
}
out.push_str(&format!("<{p}LineString><{p}coordinates>"));
write_coords(out, l, d);
out.push_str(&format!("</{p}coordinates></{p}LineString>"));
}
fn write_polygon(out: &mut String, poly: &Polygon<f64>, d: usize, p: &str) {
if poly.exterior().0.is_empty() {
return;
}
out.push_str(&format!(
"<{p}Polygon><{p}outerBoundaryIs><{p}LinearRing><{p}coordinates>"
));
write_coords(out, poly.exterior(), d);
out.push_str(&format!(
"</{p}coordinates></{p}LinearRing></{p}outerBoundaryIs>"
));
for ring in poly.interiors() {
out.push_str(&format!(
"<{p}innerBoundaryIs><{p}LinearRing><{p}coordinates>"
));
write_coords(out, ring, d);
out.push_str(&format!(
"</{p}coordinates></{p}LinearRing></{p}innerBoundaryIs>"
));
}
out.push_str(&format!("</{p}Polygon>"));
}
fn write_coords(out: &mut String, l: &LineString<f64>, d: usize) {
for (i, c) in l.0.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(&format!("{},{}", num(c.x, d), num(c.y, d)));
}
}
fn is_xml_name(s: &str) -> bool {
let mut chars = s.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::functions::io::{st_geom_from_text, st_set_srid};
fn g(wkt: &str) -> Vec<u8> {
st_set_srid(&st_geom_from_text(wkt, None).unwrap(), 4326).unwrap()
}
fn kml(wkt: &str) -> String {
st_as_kml(&g(wkt), None, None).unwrap()
}
#[test]
fn output_is_byte_identical_to_postgis() {
assert_eq!(
kml("POINT(1 2)"),
"<Point><coordinates>1,2</coordinates></Point>"
);
assert_eq!(
kml("POINT(1.23456789 2.3456789)"),
"<Point><coordinates>1.23456789,2.3456789</coordinates></Point>"
);
assert_eq!(
kml("LINESTRING(0 0,1 1,2 0)"),
"<LineString><coordinates>0,0 1,1 2,0</coordinates></LineString>"
);
assert_eq!(
kml("POLYGON((0 0,4 0,4 4,0 4,0 0))"),
"<Polygon><outerBoundaryIs><LinearRing><coordinates>0,0 4,0 4,4 0,4 0,0\
</coordinates></LinearRing></outerBoundaryIs></Polygon>"
);
assert_eq!(
kml("POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1))"),
"<Polygon><outerBoundaryIs><LinearRing><coordinates>0,0 4,0 4,4 0,4 0,0\
</coordinates></LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing>\
<coordinates>1,1 2,1 2,2 1,2 1,1</coordinates></LinearRing></innerBoundaryIs>\
</Polygon>"
);
assert_eq!(
kml("MULTIPOINT(0 0,1 1)"),
"<MultiGeometry><Point><coordinates>0,0</coordinates></Point>\
<Point><coordinates>1,1</coordinates></Point></MultiGeometry>"
);
assert_eq!(
kml("MULTILINESTRING((0 0,1 1),(2 2,3 3))"),
"<MultiGeometry><LineString><coordinates>0,0 1,1</coordinates></LineString>\
<LineString><coordinates>2,2 3,3</coordinates></LineString></MultiGeometry>"
);
assert_eq!(
kml("MULTIPOLYGON(((0 0,1 0,1 1,0 0)),((5 5,6 5,6 6,5 5)))"),
"<MultiGeometry><Polygon><outerBoundaryIs><LinearRing><coordinates>0,0 1,0 1,1 0,0\
</coordinates></LinearRing></outerBoundaryIs></Polygon><Polygon><outerBoundaryIs>\
<LinearRing><coordinates>5,5 6,5 6,6 5,5</coordinates></LinearRing>\
</outerBoundaryIs></Polygon></MultiGeometry>"
);
assert_eq!(kml("LINESTRING EMPTY"), "");
assert_eq!(kml("POLYGON EMPTY"), "");
assert!(st_as_kml(&g("GEOMETRYCOLLECTION(POINT(0 0))"), None, None).is_err());
}
#[test]
fn digits_and_prefix_match_postgis() {
assert_eq!(
st_as_kml(&g("POINT(1.23456789 2.3456789)"), Some(3), None).unwrap(),
"<Point><coordinates>1.235,2.346</coordinates></Point>"
);
assert_eq!(
st_as_kml(&g("POINT(1 2)"), Some(15), Some("kml")).unwrap(),
"<kml:Point><kml:coordinates>1,2</kml:coordinates></kml:Point>"
);
assert_eq!(
st_as_kml(&g("POLYGON((0 0,1 0,1 1,0 0))"), Some(15), Some("kml")).unwrap(),
"<kml:Polygon><kml:outerBoundaryIs><kml:LinearRing><kml:coordinates>0,0 1,0 1,1 0,0\
</kml:coordinates></kml:LinearRing></kml:outerBoundaryIs></kml:Polygon>"
);
assert_eq!(
st_as_kml(&g("POINT(1.5 2.5)"), Some(0), None).unwrap(),
"<Point><coordinates>2,2</coordinates></Point>"
);
assert!(st_as_kml(&g("POINT(1 2)"), None, Some("<script")).is_err());
assert!(st_as_kml(&g("POINT(1 2)"), None, Some("1abc")).is_err());
assert_eq!(
st_as_kml(&g("POINT(1 2)"), None, Some("")).unwrap(),
"<Point><coordinates>1,2</coordinates></Point>"
);
}
#[test]
fn the_output_is_always_wgs84() {
let webmerc = st_set_srid(&st_geom_from_text("POINT(1 2)", None).unwrap(), 3857).unwrap();
let out = st_as_kml(&webmerc, None, None).unwrap();
assert!(out.starts_with("<Point><coordinates>0.0000089"), "{out}");
let unknown = st_geom_from_text("POINT(1 2)", None).unwrap();
let err = st_as_kml(&unknown, None, None).unwrap_err().to_string();
assert!(err.contains("ST_AsKML"), "{err}");
assert!(err.contains("SRID"), "{err}");
}
}