#![doc(html_logo_url = "https://raw.githubusercontent.com/georust/meta/master/logo/logo.png")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(feature = "geo-types", doc = "```")]
#![cfg_attr(not(feature = "geo-types"), doc = "```ignore")]
#![cfg_attr(feature = "geo-types", doc = "```")]
#![cfg_attr(not(feature = "geo-types"), doc = "```ignore")]
use std::default::Default;
use std::fmt;
use std::str::FromStr;
use num_traits::{Float, Num, NumCast};
use crate::tokenizer::{PeekableTokens, Token, Tokens};
use crate::types::{
Dimension, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point,
Polygon,
};
mod to_wkt;
mod tokenizer;
pub mod types;
#[cfg(feature = "geo-types")]
extern crate geo_types;
extern crate thiserror;
pub use crate::to_wkt::ToWkt;
#[cfg(feature = "geo-types")]
#[deprecated(note = "renamed module to `wkt::geo_types_from_wkt`")]
pub mod conversion;
#[cfg(feature = "geo-types")]
pub mod geo_types_from_wkt;
#[cfg(feature = "geo-types")]
mod geo_types_to_wkt;
#[cfg(feature = "serde")]
extern crate serde;
#[cfg(feature = "serde")]
pub mod deserialize;
#[cfg(feature = "serde")]
pub use deserialize::deserialize_wkt;
mod from_wkt;
pub use from_wkt::TryFromWkt;
#[cfg(all(feature = "serde", feature = "geo-types"))]
#[allow(deprecated)]
pub use deserialize::geo_types::deserialize_geometry;
#[cfg(all(feature = "serde", feature = "geo-types"))]
#[deprecated(
since = "0.10.2",
note = "instead: use wkt::deserialize::geo_types::deserialize_point"
)]
pub use deserialize::geo_types::deserialize_point;
pub trait WktNum: Num + NumCast + PartialOrd + PartialEq + Copy + fmt::Debug {}
impl<T> WktNum for T where T: Num + NumCast + PartialOrd + PartialEq + Copy + fmt::Debug {}
pub trait WktFloat: WktNum + Float {}
impl<T> WktFloat for T where T: WktNum + Float {}
#[derive(Clone, Debug, PartialEq)]
pub enum Wkt<T>
where
T: WktNum,
{
Point(Point<T>),
LineString(LineString<T>),
Polygon(Polygon<T>),
MultiPoint(MultiPoint<T>),
MultiLineString(MultiLineString<T>),
MultiPolygon(MultiPolygon<T>),
GeometryCollection(GeometryCollection<T>),
}
impl<T> Wkt<T>
where
T: WktNum + FromStr + Default,
{
fn from_word_and_tokens(
word: &str,
tokens: &mut PeekableTokens<T>,
) -> Result<Self, &'static str> {
match word {
w if w.eq_ignore_ascii_case("POINT") => {
let x = <Point<T> as FromTokens<T>>::from_tokens_with_header(tokens, None);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("POINTZ") => {
let x = <Point<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZ),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("POINTM") => {
let x = <Point<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("POINTZM") => {
let x = <Point<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("LINESTRING") || w.eq_ignore_ascii_case("LINEARRING") => {
let x = <LineString<T> as FromTokens<T>>::from_tokens_with_header(tokens, None);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("LINESTRINGZ") => {
let x = <LineString<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZ),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("LINESTRINGM") => {
let x = <LineString<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("LINESTRINGZM") => {
let x = <LineString<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("POLYGON") => {
let x = <Polygon<T> as FromTokens<T>>::from_tokens_with_header(tokens, None);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("POLYGONZ") => {
let x = <Polygon<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZ),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("POLYGONM") => {
let x = <Polygon<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("POLYGONZM") => {
let x = <Polygon<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTIPOINT") => {
let x = <MultiPoint<T> as FromTokens<T>>::from_tokens_with_header(tokens, None);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTIPOINTZ") => {
let x = <MultiPoint<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZ),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTIPOINTM") => {
let x = <MultiPoint<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTIPOINTZM") => {
let x = <MultiPoint<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTILINESTRING") => {
let x =
<MultiLineString<T> as FromTokens<T>>::from_tokens_with_header(tokens, None);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTILINESTRINGZ") => {
let x = <MultiLineString<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZ),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTILINESTRINGM") => {
let x = <MultiLineString<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTILINESTRINGZM") => {
let x = <MultiLineString<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTIPOLYGON") => {
let x = <MultiPolygon<T> as FromTokens<T>>::from_tokens_with_header(tokens, None);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTIPOLYGONZ") => {
let x = <MultiPolygon<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZ),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTIPOLYGONM") => {
let x = <MultiPolygon<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("MULTIPOLYGONZM") => {
let x = <MultiPolygon<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("GEOMETRYCOLLECTION") => {
let x =
<GeometryCollection<T> as FromTokens<T>>::from_tokens_with_header(tokens, None);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("GEOMETRYCOLLECTIONZ") => {
let x = <GeometryCollection<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZ),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("GEOMETRYCOLLECTIONM") => {
let x = <GeometryCollection<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYM),
);
x.map(|y| y.into())
}
w if w.eq_ignore_ascii_case("GEOMETRYCOLLECTIONZM") => {
let x = <GeometryCollection<T> as FromTokens<T>>::from_tokens_with_header(
tokens,
Some(Dimension::XYZM),
);
x.map(|y| y.into())
}
_ => Err("Invalid type encountered"),
}
}
}
impl<T> fmt::Display for Wkt<T>
where
T: WktNum + fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match self {
Wkt::Point(point) => point.fmt(f),
Wkt::LineString(linestring) => linestring.fmt(f),
Wkt::Polygon(polygon) => polygon.fmt(f),
Wkt::MultiPoint(multipoint) => multipoint.fmt(f),
Wkt::MultiLineString(multilinstring) => multilinstring.fmt(f),
Wkt::MultiPolygon(multipolygon) => multipolygon.fmt(f),
Wkt::GeometryCollection(geometrycollection) => geometrycollection.fmt(f),
}
}
}
impl<T> Wkt<T>
where
T: WktNum + FromStr + Default,
{
fn from_tokens(tokens: Tokens<T>) -> Result<Self, &'static str> {
let mut tokens = tokens.peekable();
let word = match tokens.next().transpose()? {
Some(Token::Word(word)) => {
if !word.is_ascii() {
return Err("Encountered non-ascii word");
}
word
}
_ => return Err("Invalid WKT format"),
};
Wkt::from_word_and_tokens(&word, &mut tokens)
}
}
impl<T> FromStr for Wkt<T>
where
T: WktNum + FromStr + Default,
{
type Err = &'static str;
fn from_str(wkt_str: &str) -> Result<Self, Self::Err> {
Wkt::from_tokens(Tokens::from_str(wkt_str))
}
}
fn infer_geom_dimension<T: WktNum + FromStr + Default>(
tokens: &mut PeekableTokens<T>,
) -> Result<Dimension, &'static str> {
if let Some(Ok(c)) = tokens.peek() {
match c {
Token::Word(w) => match w.as_str() {
w if w.eq_ignore_ascii_case("Z") => {
tokens.next().unwrap().unwrap();
Ok(Dimension::XYZ)
}
w if w.eq_ignore_ascii_case("M") => {
tokens.next().unwrap().unwrap();
Ok(Dimension::XYM)
}
w if w.eq_ignore_ascii_case("ZM") => {
tokens.next().unwrap().unwrap();
Ok(Dimension::XYZM)
}
w if w.eq_ignore_ascii_case("EMPTY") => Ok(Dimension::XY),
_ => Err("Unexpected word before open paren"),
},
_ => Ok(Dimension::XY),
}
} else {
Err("End of stream")
}
}
trait FromTokens<T>: Sized + Default
where
T: WktNum + FromStr + Default,
{
fn from_tokens(tokens: &mut PeekableTokens<T>, dim: Dimension) -> Result<Self, &'static str>;
fn from_tokens_with_header(
tokens: &mut PeekableTokens<T>,
dim: Option<Dimension>,
) -> Result<Self, &'static str> {
let dim = if let Some(dim) = dim {
dim
} else {
infer_geom_dimension(tokens)?
};
FromTokens::from_tokens_with_parens(tokens, dim)
}
fn from_tokens_with_parens(
tokens: &mut PeekableTokens<T>,
dim: Dimension,
) -> Result<Self, &'static str> {
match tokens.next().transpose()? {
Some(Token::ParenOpen) => (),
Some(Token::Word(ref s)) if s.eq_ignore_ascii_case("EMPTY") => {
return Ok(Default::default());
}
_ => return Err("Missing open parenthesis for type"),
};
let result = FromTokens::from_tokens(tokens, dim);
match tokens.next().transpose()? {
Some(Token::ParenClose) => (),
_ => return Err("Missing closing parenthesis for type"),
};
result
}
fn from_tokens_with_optional_parens(
tokens: &mut PeekableTokens<T>,
dim: Dimension,
) -> Result<Self, &'static str> {
match tokens.peek() {
Some(Ok(Token::ParenOpen)) => Self::from_tokens_with_parens(tokens, dim),
_ => Self::from_tokens(tokens, dim),
}
}
fn comma_many<F>(
f: F,
tokens: &mut PeekableTokens<T>,
dim: Dimension,
) -> Result<Vec<Self>, &'static str>
where
F: Fn(&mut PeekableTokens<T>, Dimension) -> Result<Self, &'static str>,
{
let mut items = Vec::new();
let item = f(tokens, dim)?;
items.push(item);
while let Some(&Ok(Token::Comma)) = tokens.peek() {
tokens.next(); let item = f(tokens, dim)?;
items.push(item);
}
Ok(items)
}
}
#[cfg(test)]
mod tests {
use crate::types::{Coord, MultiPolygon, Point};
use crate::Wkt;
use std::str::FromStr;
#[test]
fn empty_string() {
let res: Result<Wkt<f64>, _> = Wkt::from_str("");
assert!(res.is_err());
}
#[test]
fn empty_items() {
let wkt: Wkt<f64> = Wkt::from_str("POINT EMPTY").ok().unwrap();
match wkt {
Wkt::Point(Point(None)) => (),
_ => unreachable!(),
};
let wkt: Wkt<f64> = Wkt::from_str("MULTIPOLYGON EMPTY").ok().unwrap();
match wkt {
Wkt::MultiPolygon(MultiPolygon(polygons)) => assert_eq!(polygons.len(), 0),
_ => unreachable!(),
};
}
#[test]
fn lowercase_point() {
let wkt: Wkt<f64> = Wkt::from_str("point EMPTY").ok().unwrap();
match wkt {
Wkt::Point(Point(None)) => (),
_ => unreachable!(),
};
}
#[test]
fn invalid_number() {
let msg = <Wkt<f64>>::from_str("POINT (10 20.1A)").unwrap_err();
assert_eq!(
"Unable to parse input number as the desired output type",
msg
);
}
#[test]
fn test_points() {
let wkt = <Wkt<f64>>::from_str("POINT (10 20.1)").ok().unwrap();
match wkt {
Wkt::Point(Point(Some(coord))) => {
assert_eq!(coord.x, 10.0);
assert_eq!(coord.y, 20.1);
assert_eq!(coord.z, None);
assert_eq!(coord.m, None);
}
_ => panic!("excepted to be parsed as a POINT"),
}
let wkt = <Wkt<f64>>::from_str("POINT Z (10 20.1 5)").ok().unwrap();
match wkt {
Wkt::Point(Point(Some(coord))) => {
assert_eq!(coord.x, 10.0);
assert_eq!(coord.y, 20.1);
assert_eq!(coord.z, Some(5.0));
assert_eq!(coord.m, None);
}
_ => panic!("excepted to be parsed as a POINT"),
}
let wkt = <Wkt<f64>>::from_str("POINT M (10 20.1 80)").ok().unwrap();
match wkt {
Wkt::Point(Point(Some(coord))) => {
assert_eq!(coord.x, 10.0);
assert_eq!(coord.y, 20.1);
assert_eq!(coord.z, None);
assert_eq!(coord.m, Some(80.0));
}
_ => panic!("excepted to be parsed as a POINT"),
}
let wkt = <Wkt<f64>>::from_str("POINT ZM (10 20.1 5 80)")
.ok()
.unwrap();
match wkt {
Wkt::Point(Point(Some(coord))) => {
assert_eq!(coord.x, 10.0);
assert_eq!(coord.y, 20.1);
assert_eq!(coord.z, Some(5.0));
assert_eq!(coord.m, Some(80.0));
}
_ => panic!("excepted to be parsed as a POINT"),
}
}
#[test]
fn support_jts_linearring() {
let wkt: Wkt<f64> = Wkt::from_str("linearring (10 20, 30 40)").ok().unwrap();
match wkt {
Wkt::LineString(_ls) => (),
_ => panic!("expected to be parsed as a LINESTRING"),
};
}
#[test]
fn test_debug() {
let g = Wkt::Point(Point(Some(Coord {
x: 1.0,
y: 2.0,
m: None,
z: None,
})));
assert_eq!(
format!("{:?}", g),
"Point(Point(Some(Coord { x: 1.0, y: 2.0, z: None, m: None })))"
);
}
#[test]
fn test_display_on_wkt() {
let wktls: Wkt<f64> = Wkt::from_str("LINESTRING(10 20, 20 30)").unwrap();
assert_eq!(wktls.to_string(), "LINESTRING(10 20,20 30)");
}
}