use alloc::string::String;
use geometry_cs::CoordinateSystem;
use geometry_model::{
DynGeometry, Linestring, MultiLinestring, MultiPoint, MultiPolygon, Point, Polygon, Ring,
};
use geometry_trait::{
Geometry, Linestring as LinestringTrait, MultiLinestring as MultiLinestringTrait,
MultiPoint as MultiPointTrait, MultiPolygon as MultiPolygonTrait, Point as PointTrait,
Polygon as PolygonTrait, Ring as RingTrait,
};
#[must_use]
pub fn to_wkt<G: Geometry + WriteWkt>(g: &G) -> String {
let mut out = String::new();
let _ = g.write_wkt(&mut out);
out
}
pub fn write_wkt<G: WriteWkt, W: core::fmt::Write>(g: &G, out: &mut W) -> core::fmt::Result {
g.write_wkt(out)
}
#[doc(hidden)]
pub trait WriteWkt {
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result;
}
fn write_scalar(out: &mut dyn core::fmt::Write, v: f64) -> core::fmt::Result {
if v.is_finite() && v.fract() == 0.0 {
if v.abs() < 9.007_199_254_740_992e15 {
#[allow(
clippy::cast_possible_truncation,
reason = "guarded by the magnitude check above; v is integer-valued"
)]
return write!(out, "{}", v as i64);
}
}
write!(out, "{v}")
}
fn write_coords<P: PointTrait<Scalar = f64>>(
out: &mut dyn core::fmt::Write,
p: &P,
) -> core::fmt::Result {
write_scalar(out, p.get::<0>())?;
out.write_char(' ')?;
write_scalar(out, p.get::<1>())
}
fn write_point_seq<'a, P, I>(out: &mut dyn core::fmt::Write, points: I) -> core::fmt::Result
where
P: PointTrait<Scalar = f64> + 'a,
I: Iterator<Item = &'a P>,
{
for (i, p) in points.enumerate() {
if i > 0 {
out.write_char(',')?;
}
write_coords(out, p)?;
}
Ok(())
}
fn write_polygon_rings<Pg>(out: &mut dyn core::fmt::Write, pg: &Pg) -> core::fmt::Result
where
Pg: PolygonTrait,
Pg::Point: PointTrait<Scalar = f64>,
{
out.write_char('(')?;
out.write_char('(')?;
write_point_seq(out, pg.exterior().points())?;
out.write_char(')')?;
for ring in pg.interiors() {
out.write_char(',')?;
out.write_char('(')?;
write_point_seq(out, ring.points())?;
out.write_char(')')?;
}
out.write_char(')')
}
impl<Cs: CoordinateSystem> WriteWkt for Point<f64, 2, Cs> {
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result {
out.write_str("POINT(")?;
write_coords(out, self)?;
out.write_char(')')
}
}
impl<P: PointTrait<Scalar = f64>> WriteWkt for Linestring<P> {
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result {
if self.points().next().is_none() {
return out.write_str("LINESTRING EMPTY");
}
out.write_str("LINESTRING(")?;
write_point_seq(out, self.points())?;
out.write_char(')')
}
}
impl<P: PointTrait<Scalar = f64>> WriteWkt for Ring<P, true, true> {
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result {
if self.points().next().is_none() {
return out.write_str("POLYGON EMPTY");
}
out.write_str("POLYGON((")?;
write_point_seq(out, self.points())?;
out.write_str("))")
}
}
impl<P: PointTrait<Scalar = f64>> WriteWkt for Polygon<P, true, true> {
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result {
if self.exterior().points().next().is_none() {
return out.write_str("POLYGON EMPTY");
}
out.write_str("POLYGON")?;
write_polygon_rings(out, self)
}
}
impl<P: PointTrait<Scalar = f64>> WriteWkt for MultiPoint<P> {
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result {
if self.points().next().is_none() {
return out.write_str("MULTIPOINT EMPTY");
}
out.write_str("MULTIPOINT(")?;
for (i, p) in self.points().enumerate() {
if i > 0 {
out.write_char(',')?;
}
out.write_char('(')?;
write_coords(out, p)?;
out.write_char(')')?;
}
out.write_char(')')
}
}
impl<L> WriteWkt for MultiLinestring<L>
where
L: LinestringTrait,
L::Point: PointTrait<Scalar = f64>,
{
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result {
if self.linestrings().next().is_none() {
return out.write_str("MULTILINESTRING EMPTY");
}
out.write_str("MULTILINESTRING(")?;
for (i, ls) in self.linestrings().enumerate() {
if i > 0 {
out.write_char(',')?;
}
out.write_char('(')?;
write_point_seq(out, ls.points())?;
out.write_char(')')?;
}
out.write_char(')')
}
}
impl<Pg> WriteWkt for MultiPolygon<Pg>
where
Pg: PolygonTrait,
Pg::Point: PointTrait<Scalar = f64>,
{
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result {
if self.polygons().next().is_none() {
return out.write_str("MULTIPOLYGON EMPTY");
}
out.write_str("MULTIPOLYGON(")?;
for (i, pg) in self.polygons().enumerate() {
if i > 0 {
out.write_char(',')?;
}
write_polygon_rings(out, pg)?;
}
out.write_char(')')
}
}
impl<Cs: CoordinateSystem> WriteWkt for DynGeometry<f64, Cs> {
fn write_wkt(&self, out: &mut dyn core::fmt::Write) -> core::fmt::Result {
enum Frag<'a, Cs: CoordinateSystem> {
Geom(&'a DynGeometry<f64, Cs>),
Lit(&'static str),
}
let mut stack = alloc::vec![Frag::Geom(self)];
while let Some(frag) = stack.pop() {
match frag {
Frag::Lit(s) => out.write_str(s)?,
Frag::Geom(g) => match g {
DynGeometry::Point(p) => p.write_wkt(out)?,
DynGeometry::LineString(ls) => ls.write_wkt(out)?,
DynGeometry::Polygon(pg) => pg.write_wkt(out)?,
DynGeometry::MultiPoint(mp) => mp.write_wkt(out)?,
DynGeometry::MultiLineString(mls) => mls.write_wkt(out)?,
DynGeometry::MultiPolygon(mpg) => mpg.write_wkt(out)?,
DynGeometry::GeometryCollection(items) => {
if items.is_empty() {
out.write_str("GEOMETRYCOLLECTION EMPTY")?;
continue;
}
stack.push(Frag::Lit(")"));
for (i, item) in items.iter().enumerate().rev() {
stack.push(Frag::Geom(item));
if i > 0 {
stack.push(Frag::Lit(","));
}
}
stack.push(Frag::Lit("GEOMETRYCOLLECTION("));
}
},
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::float_cmp,
reason = "coordinates are exact integer literals in these fixtures"
)]
use super::*;
use alloc::vec;
use geometry_cs::Cartesian;
use geometry_model::Point2D;
type Pt = Point2D<f64, Cartesian>;
#[test]
fn point_canonical() {
let p = Pt::new(10.0, 10.0);
assert_eq!(to_wkt(&p), "POINT(10 10)");
}
#[test]
fn nested_collection_writer_is_iterative_and_correct() {
use geometry_model::DynGeometry;
let g = DynGeometry::<f64, Cartesian>::GeometryCollection(vec![
DynGeometry::Point(Pt::new(1.0, 1.0)),
DynGeometry::GeometryCollection(vec![DynGeometry::Point(Pt::new(2.0, 2.0))]),
]);
assert_eq!(
to_wkt(&g),
"GEOMETRYCOLLECTION(POINT(1 1),GEOMETRYCOLLECTION(POINT(2 2)))"
);
let mut deep = DynGeometry::<f64, Cartesian>::Point(Pt::new(0.0, 0.0));
for _ in 0..200_000 {
deep = DynGeometry::GeometryCollection(vec![deep]);
}
assert!(to_wkt(&deep).starts_with("GEOMETRYCOLLECTION("));
core::mem::forget(deep); }
#[test]
fn fractional_coord_round_trips() {
let p = Pt::new(1.5, -2.25);
assert_eq!(to_wkt(&p), "POINT(1.5 -2.25)");
}
#[test]
fn linestring_canonical() {
let ls = Linestring(vec![
Pt::new(10.0, 10.0),
Pt::new(20.0, 20.0),
Pt::new(30.0, 40.0),
]);
assert_eq!(to_wkt(&ls), "LINESTRING(10 10,20 20,30 40)");
}
#[test]
fn polygon_with_hole_canonical() {
let outer = Ring::from_vec(vec![
Pt::new(0.0, 0.0),
Pt::new(0.0, 10.0),
Pt::new(10.0, 10.0),
Pt::new(10.0, 0.0),
Pt::new(0.0, 0.0),
]);
let hole = Ring::from_vec(vec![
Pt::new(2.0, 2.0),
Pt::new(2.0, 4.0),
Pt::new(4.0, 4.0),
Pt::new(4.0, 2.0),
Pt::new(2.0, 2.0),
]);
let poly = Polygon::with_inners(outer, vec![hole]);
assert_eq!(
to_wkt(&poly),
"POLYGON((0 0,0 10,10 10,10 0,0 0),(2 2,2 4,4 4,4 2,2 2))"
);
}
#[test]
fn multipoint_canonical() {
let mp = MultiPoint(vec![Pt::new(10.0, 10.0), Pt::new(20.0, 20.0)]);
assert_eq!(to_wkt(&mp), "MULTIPOINT((10 10),(20 20))");
}
#[test]
fn geometry_collection_canonical() {
let g = DynGeometry::<f64, Cartesian>::GeometryCollection(vec![
DynGeometry::Point(Pt::new(10.0, 10.0)),
DynGeometry::LineString(Linestring(vec![Pt::new(10.0, 10.0), Pt::new(20.0, 20.0)])),
]);
assert_eq!(
to_wkt(&g),
"GEOMETRYCOLLECTION(POINT(10 10),LINESTRING(10 10,20 20))"
);
}
}