diff --git a/CHANGES.md b/CHANGES.md index 60cf3e8..2d07e61 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased +* Support parsing Z, M, and ZM WKT strings. * Changed license field to [SPDX 2.1 license expression](https://spdx.dev/spdx-specification-21-web-version/#h.jxpfx0ykyb60) * Bump min version of geo-types, and update geo_types::Coordinate to non-deprecated geo_types::Coord * BREAKING: WktNum must implement PartialEq diff --git a/src/lib.rs b/src/lib.rs index 3cb3f8c..2b97b13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,13 +83,10 @@ use std::str::FromStr; use num_traits::{Float, Num, NumCast}; use crate::tokenizer::{PeekableTokens, Token, Tokens}; -use crate::types::GeometryCollection; -use crate::types::LineString; -use crate::types::MultiLineString; -use crate::types::MultiPoint; -use crate::types::MultiPolygon; -use crate::types::Point; -use crate::types::Polygon; +use crate::types::{ + Dimension, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, + Polygon, +}; mod to_wkt; mod tokenizer; @@ -162,62 +159,186 @@ where word: &str, tokens: &mut PeekableTokens, ) -> Result { + // Normally Z/M/ZM is separated by a space from the primary WKT word. E.g. `POINT Z` + // instead of `POINTZ`. However we wish to support both types (in reading). When written + // without a space, `POINTZ` is considered a single word, which means we need to include + // matches here. match word { w if w.eq_ignore_ascii_case("POINT") => { - let x = as FromTokens>::from_tokens_with_parens(tokens); + let x = as FromTokens>::from_tokens_with_header(tokens, None); x.map(|y| y.into()) } w if w.eq_ignore_ascii_case("POINTZ") => { - let x = as FromTokens>::from_tokens_with_parens(tokens)?; - if let Some(coord) = &x.0 { - if coord.z.is_none() { - return Err("POINTZ must have a z-coordinate."); - } - } - Ok(x.into()) + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZ), + ); + x.map(|y| y.into()) } w if w.eq_ignore_ascii_case("POINTM") => { - let mut x = as FromTokens>::from_tokens_with_parens(tokens)?; - if let Some(coord) = &mut x.0 { - if coord.z.is_none() { - return Err("POINTM must have an m-coordinate."); - } else { - coord.m = coord.z.take(); - } - } - Ok(x.into()) + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYM), + ); + x.map(|y| y.into()) } w if w.eq_ignore_ascii_case("POINTZM") => { - let x = as FromTokens>::from_tokens_with_parens(tokens)?; - if let Some(coord) = &x.0 { - if coord.z.is_none() || coord.m.is_none() { - return Err("POINTZM must have both a z- and m-coordinate"); - } - } - Ok(x.into()) + let x = as FromTokens>::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 = as FromTokens>::from_tokens_with_parens(tokens); + let x = as FromTokens>::from_tokens_with_header(tokens, None); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("LINESTRINGZ") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZ), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("LINESTRINGM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYM), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("LINESTRINGZM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZM), + ); x.map(|y| y.into()) } w if w.eq_ignore_ascii_case("POLYGON") => { - let x = as FromTokens>::from_tokens_with_parens(tokens); + let x = as FromTokens>::from_tokens_with_header(tokens, None); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("POLYGONZ") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZ), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("POLYGONM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYM), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("POLYGONZM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZM), + ); x.map(|y| y.into()) } w if w.eq_ignore_ascii_case("MULTIPOINT") => { - let x = as FromTokens>::from_tokens_with_parens(tokens); + let x = as FromTokens>::from_tokens_with_header(tokens, None); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTIPOINTZ") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZ), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTIPOINTM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYM), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTIPOINTZM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZM), + ); x.map(|y| y.into()) } w if w.eq_ignore_ascii_case("MULTILINESTRING") => { - let x = as FromTokens>::from_tokens_with_parens(tokens); + let x = + as FromTokens>::from_tokens_with_header(tokens, None); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTILINESTRINGZ") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZ), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTILINESTRINGM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYM), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTILINESTRINGZM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZM), + ); x.map(|y| y.into()) } w if w.eq_ignore_ascii_case("MULTIPOLYGON") => { - let x = as FromTokens>::from_tokens_with_parens(tokens); + let x = as FromTokens>::from_tokens_with_header(tokens, None); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTIPOLYGONZ") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZ), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTIPOLYGONM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYM), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("MULTIPOLYGONZM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZM), + ); x.map(|y| y.into()) } w if w.eq_ignore_ascii_case("GEOMETRYCOLLECTION") => { - let x = as FromTokens>::from_tokens_with_parens(tokens); + let x = + as FromTokens>::from_tokens_with_header(tokens, None); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("GEOMETRYCOLLECTIONZ") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZ), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("GEOMETRYCOLLECTIONM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYM), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("GEOMETRYCOLLECTIONZM") => { + let x = as FromTokens>::from_tokens_with_header( + tokens, + Some(Dimension::XYZM), + ); x.map(|y| y.into()) } _ => Err("Invalid type encountered"), @@ -272,21 +393,72 @@ where } } +fn infer_geom_dimension( + tokens: &mut PeekableTokens, +) -> Result { + if let Some(Ok(c)) = tokens.peek() { + match c { + // If we match a word check if it's Z/M/ZM and consume the token from the stream + 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"), + }, + // Not a word, e.g. an open paren + _ => Ok(Dimension::XY), + } + } else { + Err("End of stream") + } +} + trait FromTokens: Sized + Default where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result; + fn from_tokens(tokens: &mut PeekableTokens, dim: Dimension) -> Result; + + /// The preferred top-level FromTokens API, which additionally checks for the presence of Z, M, + /// and ZM in the token stream. + fn from_tokens_with_header( + tokens: &mut PeekableTokens, + dim: Option, + ) -> Result { + 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) -> Result { + fn from_tokens_with_parens( + tokens: &mut PeekableTokens, + dim: Dimension, + ) -> Result { match tokens.next().transpose()? { Some(Token::ParenOpen) => (), Some(Token::Word(ref s)) if s.eq_ignore_ascii_case("EMPTY") => { - return Ok(Default::default()) + // TODO: expand this to support Z EMPTY + // Maybe create a DefaultXY, DefaultXYZ trait etc for each geometry type, and then + // here match on the dim to decide which default trait to use. + return Ok(Default::default()); } _ => return Err("Missing open parenthesis for type"), }; - let result = FromTokens::from_tokens(tokens); + let result = FromTokens::from_tokens(tokens, dim); match tokens.next().transpose()? { Some(Token::ParenClose) => (), _ => return Err("Missing closing parenthesis for type"), @@ -296,26 +468,31 @@ where fn from_tokens_with_optional_parens( tokens: &mut PeekableTokens, + dim: Dimension, ) -> Result { match tokens.peek() { - Some(Ok(Token::ParenOpen)) => Self::from_tokens_with_parens(tokens), - _ => Self::from_tokens(tokens), + Some(Ok(Token::ParenOpen)) => Self::from_tokens_with_parens(tokens, dim), + _ => Self::from_tokens(tokens, dim), } } - fn comma_many(f: F, tokens: &mut PeekableTokens) -> Result, &'static str> + fn comma_many( + f: F, + tokens: &mut PeekableTokens, + dim: Dimension, + ) -> Result, &'static str> where - F: Fn(&mut PeekableTokens) -> Result, + F: Fn(&mut PeekableTokens, Dimension) -> Result, { let mut items = Vec::new(); - let item = f(tokens)?; + let item = f(tokens, dim)?; items.push(item); while let Some(&Ok(Token::Comma)) = tokens.peek() { tokens.next(); // throw away comma - let item = f(tokens)?; + let item = f(tokens, dim)?; items.push(item); } @@ -383,7 +560,7 @@ mod tests { } // point(x, y, z) - let wkt = >::from_str("POINTZ (10 20.1 5)").ok().unwrap(); + let wkt = >::from_str("POINT Z (10 20.1 5)").ok().unwrap(); match wkt { Wkt::Point(Point(Some(coord))) => { assert_eq!(coord.x, 10.0); @@ -395,7 +572,7 @@ mod tests { } // point(x, y, m) - let wkt = >::from_str("POINTM (10 20.1 80)").ok().unwrap(); + let wkt = >::from_str("POINT M (10 20.1 80)").ok().unwrap(); match wkt { Wkt::Point(Point(Some(coord))) => { assert_eq!(coord.x, 10.0); @@ -407,7 +584,9 @@ mod tests { } // point(x, y, z, m) - let wkt = >::from_str("POINTZM (10 20.1 5 80)").ok().unwrap(); + let wkt = >::from_str("POINT ZM (10 20.1 5 80)") + .ok() + .unwrap(); match wkt { Wkt::Point(Point(Some(coord))) => { assert_eq!(coord.x, 10.0); diff --git a/src/types/coord.rs b/src/types/coord.rs index bb42c9e..358490e 100644 --- a/src/types/coord.rs +++ b/src/types/coord.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::tokenizer::{PeekableTokens, Token}; +use crate::types::Dimension; use crate::{FromTokens, WktNum}; use std::fmt; use std::str::FromStr; @@ -48,7 +49,7 @@ impl FromTokens for Coord where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result { + fn from_tokens(tokens: &mut PeekableTokens, dim: Dimension) -> Result { let x = match tokens.next().transpose()? { Some(Token::Number(n)) => n, _ => return Err("Expected a number for the X coordinate"), @@ -61,17 +62,33 @@ where let mut z = None; let mut m = None; - if let Some(Ok(Token::Number(_))) = tokens.peek() { - z = match tokens.next().transpose()? { - Some(Token::Number(n)) => Some(n), - _ => None, - }; - - if let Some(Ok(Token::Number(_))) = tokens.peek() { - m = match tokens.next().transpose()? { - Some(Token::Number(n)) => Some(n), - _ => None, - }; + match dim { + Dimension::XY => (), + Dimension::XYZ => match tokens.next().transpose()? { + Some(Token::Number(n)) => { + z = Some(n); + } + _ => return Err("Expected a number for the Z coordinate"), + }, + Dimension::XYM => match tokens.next().transpose()? { + Some(Token::Number(n)) => { + m = Some(n); + } + _ => return Err("Expected a number for the M coordinate"), + }, + Dimension::XYZM => { + match tokens.next().transpose()? { + Some(Token::Number(n)) => { + z = Some(n); + } + _ => return Err("Expected a number for the Z coordinate"), + } + match tokens.next().transpose()? { + Some(Token::Number(n)) => { + m = Some(n); + } + _ => return Err("Expected a number for the M coordinate"), + } } } diff --git a/src/types/dimension.rs b/src/types/dimension.rs new file mode 100644 index 0000000..b2cbf62 --- /dev/null +++ b/src/types/dimension.rs @@ -0,0 +1,10 @@ +/// The dimension of geometry that we're parsing. +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Copy, Debug, Default)] +pub enum Dimension { + #[default] + XY, + XYZ, + XYM, + XYZM, +} diff --git a/src/types/geometrycollection.rs b/src/types/geometrycollection.rs index fd57235..8aa3d8a 100644 --- a/src/types/geometrycollection.rs +++ b/src/types/geometrycollection.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::tokenizer::{PeekableTokens, Token}; +use crate::types::Dimension; use crate::{FromTokens, Wkt, WktNum}; use std::fmt; use std::str::FromStr; @@ -53,7 +54,10 @@ impl FromTokens for GeometryCollection where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result { + // Unsure if the dimension should be used in parsing GeometryCollection; is it + // GEOMETRYCOLLECTION ( POINT Z (...) , POINT ZM (...)) + // or does a geometry collection have a known dimension? + fn from_tokens(tokens: &mut PeekableTokens, _dim: Dimension) -> Result { let mut items = Vec::new(); let word = match tokens.next().transpose()? { diff --git a/src/types/linestring.rs b/src/types/linestring.rs index 3cf99de..ff84bcf 100644 --- a/src/types/linestring.rs +++ b/src/types/linestring.rs @@ -14,6 +14,7 @@ use crate::tokenizer::PeekableTokens; use crate::types::coord::Coord; +use crate::types::Dimension; use crate::{FromTokens, Wkt, WktNum}; use std::fmt; use std::str::FromStr; @@ -34,8 +35,8 @@ impl FromTokens for LineString where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result { - let result = FromTokens::comma_many( as FromTokens>::from_tokens, tokens); + fn from_tokens(tokens: &mut PeekableTokens, dim: Dimension) -> Result { + let result = FromTokens::comma_many( as FromTokens>::from_tokens, tokens, dim); result.map(LineString) } } @@ -86,6 +87,94 @@ mod tests { assert_eq!(None, coords[1].m); } + #[test] + fn basic_linestring_z() { + let wkt = Wkt::from_str("LINESTRING Z (-117 33 2, -116 34 4)") + .ok() + .unwrap(); + let coords = match wkt { + Wkt::LineString(LineString(coords)) => coords, + _ => unreachable!(), + }; + assert_eq!(2, coords.len()); + + assert_eq!(-117.0, coords[0].x); + assert_eq!(33.0, coords[0].y); + assert_eq!(Some(2.0), coords[0].z); + assert_eq!(None, coords[0].m); + + assert_eq!(-116.0, coords[1].x); + assert_eq!(34.0, coords[1].y); + assert_eq!(Some(4.0), coords[1].z); + assert_eq!(None, coords[1].m); + } + + #[test] + fn basic_linestring_m() { + let wkt = Wkt::from_str("LINESTRING M (-117 33 2, -116 34 4)") + .ok() + .unwrap(); + let coords = match wkt { + Wkt::LineString(LineString(coords)) => coords, + _ => unreachable!(), + }; + assert_eq!(2, coords.len()); + + assert_eq!(-117.0, coords[0].x); + assert_eq!(33.0, coords[0].y); + assert_eq!(None, coords[0].z); + assert_eq!(Some(2.0), coords[0].m); + + assert_eq!(-116.0, coords[1].x); + assert_eq!(34.0, coords[1].y); + assert_eq!(None, coords[1].z); + assert_eq!(Some(4.0), coords[1].m); + } + + #[test] + fn basic_linestring_zm() { + let wkt = Wkt::from_str("LINESTRING ZM (-117 33 2 3, -116 34 4 5)") + .ok() + .unwrap(); + let coords = match wkt { + Wkt::LineString(LineString(coords)) => coords, + _ => unreachable!(), + }; + assert_eq!(2, coords.len()); + + assert_eq!(-117.0, coords[0].x); + assert_eq!(33.0, coords[0].y); + assert_eq!(Some(2.0), coords[0].z); + assert_eq!(Some(3.0), coords[0].m); + + assert_eq!(-116.0, coords[1].x); + assert_eq!(34.0, coords[1].y); + assert_eq!(Some(4.0), coords[1].z); + assert_eq!(Some(5.0), coords[1].m); + } + + #[test] + fn basic_linestring_zm_one_word() { + let wkt = Wkt::from_str("LINESTRINGZM (-117 33 2 3, -116 34 4 5)") + .ok() + .unwrap(); + let coords = match wkt { + Wkt::LineString(LineString(coords)) => coords, + _ => unreachable!(), + }; + assert_eq!(2, coords.len()); + + assert_eq!(-117.0, coords[0].x); + assert_eq!(33.0, coords[0].y); + assert_eq!(Some(2.0), coords[0].z); + assert_eq!(Some(3.0), coords[0].m); + + assert_eq!(-116.0, coords[1].x); + assert_eq!(34.0, coords[1].y); + assert_eq!(Some(4.0), coords[1].z); + assert_eq!(Some(5.0), coords[1].m); + } + #[test] fn write_empty_linestring() { let linestring: LineString = LineString(vec![]); diff --git a/src/types/mod.rs b/src/types/mod.rs index 8322721..b0dfc87 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. pub use self::coord::Coord; +pub(crate) use self::dimension::Dimension; pub use self::geometrycollection::GeometryCollection; pub use self::linestring::LineString; pub use self::multilinestring::MultiLineString; @@ -22,6 +23,7 @@ pub use self::point::Point; pub use self::polygon::Polygon; mod coord; +mod dimension; mod geometrycollection; mod linestring; mod multilinestring; diff --git a/src/types/multilinestring.rs b/src/types/multilinestring.rs index 82e9e76..dae4532 100644 --- a/src/types/multilinestring.rs +++ b/src/types/multilinestring.rs @@ -14,6 +14,7 @@ use crate::tokenizer::PeekableTokens; use crate::types::linestring::LineString; +use crate::types::Dimension; use crate::{FromTokens, Wkt, WktNum}; use std::fmt; use std::str::FromStr; @@ -59,10 +60,11 @@ impl FromTokens for MultiLineString where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result { + fn from_tokens(tokens: &mut PeekableTokens, dim: Dimension) -> Result { let result = FromTokens::comma_many( as FromTokens>::from_tokens_with_parens, tokens, + dim, ); result.map(MultiLineString) } diff --git a/src/types/multipoint.rs b/src/types/multipoint.rs index 44170b2..b0aa431 100644 --- a/src/types/multipoint.rs +++ b/src/types/multipoint.rs @@ -14,6 +14,7 @@ use crate::tokenizer::PeekableTokens; use crate::types::point::Point; +use crate::types::Dimension; use crate::{FromTokens, Wkt, WktNum}; use std::fmt; use std::str::FromStr; @@ -55,10 +56,11 @@ impl FromTokens for MultiPoint where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result { + fn from_tokens(tokens: &mut PeekableTokens, dim: Dimension) -> Result { let result = FromTokens::comma_many( as FromTokens>::from_tokens_with_optional_parens, tokens, + dim, ); result.map(MultiPoint) } @@ -81,6 +83,49 @@ mod tests { assert_eq!(2, points.len()); } + #[test] + fn basic_multipoint_zm() { + let wkt: Wkt = Wkt::from_str("MULTIPOINT ZM (0 0 4 3, 1 2 4 5)") + .ok() + .unwrap(); + let points = match wkt { + Wkt::MultiPoint(MultiPoint(points)) => points, + _ => unreachable!(), + }; + assert_eq!(2, points.len()); + + assert_eq!(0.0, points[0].0.as_ref().unwrap().x); + assert_eq!(0.0, points[0].0.as_ref().unwrap().y); + assert_eq!(Some(4.0), points[0].0.as_ref().unwrap().z); + assert_eq!(Some(3.0), points[0].0.as_ref().unwrap().m); + + assert_eq!(1.0, points[1].0.as_ref().unwrap().x); + assert_eq!(2.0, points[1].0.as_ref().unwrap().y); + assert_eq!(Some(4.0), points[1].0.as_ref().unwrap().z); + assert_eq!(Some(5.0), points[1].0.as_ref().unwrap().m); + } + + #[test] + fn basic_multipoint_zm_extra_parents() { + let wkt: Wkt = Wkt::from_str("MULTIPOINT ZM ((0 0 4 3), (1 2 4 5))") + .ok() + .unwrap(); + let points = match wkt { + Wkt::MultiPoint(MultiPoint(points)) => points, + _ => unreachable!(), + }; + assert_eq!(2, points.len()); + + assert_eq!(0.0, points[0].0.as_ref().unwrap().x); + assert_eq!(0.0, points[0].0.as_ref().unwrap().y); + assert_eq!(Some(4.0), points[0].0.as_ref().unwrap().z); + assert_eq!(Some(3.0), points[0].0.as_ref().unwrap().m); + + assert_eq!(1.0, points[1].0.as_ref().unwrap().x); + assert_eq!(2.0, points[1].0.as_ref().unwrap().y); + assert_eq!(Some(4.0), points[1].0.as_ref().unwrap().z); + assert_eq!(Some(5.0), points[1].0.as_ref().unwrap().m); + } #[test] fn postgis_style_multipoint() { let wkt: Wkt = Wkt::from_str("MULTIPOINT (8 4, 4 0)").unwrap(); diff --git a/src/types/multipolygon.rs b/src/types/multipolygon.rs index 706a493..7c21a4b 100644 --- a/src/types/multipolygon.rs +++ b/src/types/multipolygon.rs @@ -14,6 +14,7 @@ use crate::tokenizer::PeekableTokens; use crate::types::polygon::Polygon; +use crate::types::Dimension; use crate::{FromTokens, Wkt, WktNum}; use std::fmt; use std::str::FromStr; @@ -64,10 +65,11 @@ impl FromTokens for MultiPolygon where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result { + fn from_tokens(tokens: &mut PeekableTokens, dim: Dimension) -> Result { let result = FromTokens::comma_many( as FromTokens>::from_tokens_with_parens, tokens, + dim, ); result.map(MultiPolygon) } diff --git a/src/types/point.rs b/src/types/point.rs index 4521b9d..a4d6123 100644 --- a/src/types/point.rs +++ b/src/types/point.rs @@ -14,6 +14,7 @@ use crate::tokenizer::PeekableTokens; use crate::types::coord::Coord; +use crate::types::Dimension; use crate::{FromTokens, Wkt, WktNum}; use std::fmt; use std::str::FromStr; @@ -59,8 +60,8 @@ impl FromTokens for Point where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result { - let result = as FromTokens>::from_tokens(tokens); + fn from_tokens(tokens: &mut PeekableTokens, dim: Dimension) -> Result { + let result = as FromTokens>::from_tokens(tokens, dim); result.map(|coord| Point(Some(coord))) } } @@ -84,6 +85,32 @@ mod tests { assert_eq!(None, coord.m); } + #[test] + fn basic_point_z() { + let wkt = Wkt::from_str("POINT Z(-117 33 10)").ok().unwrap(); + let coord = match wkt { + Wkt::Point(Point(Some(coord))) => coord, + _ => unreachable!(), + }; + assert_eq!(-117.0, coord.x); + assert_eq!(33.0, coord.y); + assert_eq!(Some(10.0), coord.z); + assert_eq!(None, coord.m); + } + + #[test] + fn basic_point_z_one_word() { + let wkt = Wkt::from_str("POINTZ(-117 33 10)").ok().unwrap(); + let coord = match wkt { + Wkt::Point(Point(Some(coord))) => coord, + _ => unreachable!(), + }; + assert_eq!(-117.0, coord.x); + assert_eq!(33.0, coord.y); + assert_eq!(Some(10.0), coord.z); + assert_eq!(None, coord.m); + } + #[test] fn basic_point_whitespace() { let wkt: Wkt = Wkt::from_str(" \n\t\rPOINT \n\t\r( \n\r\t10 \n\t\r-20 \n\t\r) \n\t\r") diff --git a/src/types/polygon.rs b/src/types/polygon.rs index 5e547a5..fd2d5de 100644 --- a/src/types/polygon.rs +++ b/src/types/polygon.rs @@ -14,6 +14,7 @@ use crate::tokenizer::PeekableTokens; use crate::types::linestring::LineString; +use crate::types::Dimension; use crate::{FromTokens, Wkt, WktNum}; use std::fmt; use std::str::FromStr; @@ -59,10 +60,11 @@ impl FromTokens for Polygon where T: WktNum + FromStr + Default, { - fn from_tokens(tokens: &mut PeekableTokens) -> Result { + fn from_tokens(tokens: &mut PeekableTokens, dim: Dimension) -> Result { let result = FromTokens::comma_many( as FromTokens>::from_tokens_with_parens, tokens, + dim, ); result.map(Polygon) }