From e1d838de2218f410422da28a23c26a7f6329505d Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 17 Jul 2024 15:53:36 -0400 Subject: [PATCH 01/10] Support parsing Z, M, and ZM --- src/lib.rs | 121 +++++++++++++++++++------------- src/types/coord.rs | 41 +++++++---- src/types/dimension.rs | 10 +++ src/types/geometrycollection.rs | 6 +- src/types/linestring.rs | 5 +- src/types/mod.rs | 2 + src/types/multilinestring.rs | 4 +- src/types/multipoint.rs | 4 +- src/types/multipolygon.rs | 4 +- src/types/point.rs | 5 +- src/types/polygon.rs | 4 +- 11 files changed, 135 insertions(+), 71 deletions(-) create mode 100644 src/types/dimension.rs diff --git a/src/lib.rs b/src/lib.rs index b6f3f67..c28e3cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,13 +84,13 @@ 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}; mod to_wkt; mod tokenizer; @@ -165,60 +165,31 @@ where ) -> Result { 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); x.map(|y| y.as_item()) } - 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.as_item()) - } - 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.as_item()) - } - 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.as_item()) - } 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); x.map(|y| y.as_item()) } 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); x.map(|y| y.as_item()) } 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); x.map(|y| y.as_item()) } 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); x.map(|y| y.as_item()) } 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); x.map(|y| y.as_item()) } 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); x.map(|y| y.as_item()) } _ => Err("Invalid type encountered"), @@ -293,21 +264,64 @@ 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(x) => match x.as_str() { + "Z" => { + tokens.next().unwrap().unwrap(); + Ok(Dimension::XYZ) + } + "M" => { + tokens.next().unwrap().unwrap(); + + Ok(Dimension::XYM) + } + "ZM" => { + tokens.next().unwrap().unwrap(); + Ok(Dimension::XYZM) + } + _ => 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; - fn from_tokens_with_parens(tokens: &mut PeekableTokens) -> 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) -> Result { + let dim = infer_geom_dimension(tokens)?; + FromTokens::from_tokens_with_parens(tokens, dim) + } + + 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"), @@ -317,26 +331,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); } @@ -404,7 +423,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.item { Geometry::Point(Point(Some(coord))) => { assert_eq!(coord.x, 10.0); @@ -416,7 +435,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.item { Geometry::Point(Point(Some(coord))) => { assert_eq!(coord.x, 10.0); @@ -428,7 +447,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.item { Geometry::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 b1e73a5..cac922a 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, Geometry, 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 dfe6813..37488b7 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, Geometry, 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) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 8322721..097d7f9 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 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 7a65a55..ffe865a 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, Geometry, 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 051d206..3858fb3 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, Geometry, 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) } diff --git a/src/types/multipolygon.rs b/src/types/multipolygon.rs index b76956b..a63983b 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, Geometry, 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 66b3ebc..85ab102 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, Geometry, 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))) } } diff --git a/src/types/polygon.rs b/src/types/polygon.rs index 1c778c2..a5b401e 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, Geometry, 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) } From 7c9f9a16527198b08a1d5117ee504b658a149c18 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 17 Jul 2024 16:01:00 -0400 Subject: [PATCH 02/10] Add tests --- src/types/linestring.rs | 66 +++++++++++++++++++++++++++++++++++++++++ src/types/point.rs | 13 ++++++++ 2 files changed, 79 insertions(+) diff --git a/src/types/linestring.rs b/src/types/linestring.rs index 37488b7..ff68542 100644 --- a/src/types/linestring.rs +++ b/src/types/linestring.rs @@ -87,6 +87,72 @@ 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.item { + Geometry::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.item { + Geometry::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.item { + Geometry::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/point.rs b/src/types/point.rs index 85ab102..033953a 100644 --- a/src/types/point.rs +++ b/src/types/point.rs @@ -85,6 +85,19 @@ 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.item { + Geometry::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") From 57365e69fb4a70964623e8f21d1e2e9013c40110 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 17 Jul 2024 16:05:11 -0400 Subject: [PATCH 03/10] Add multipoint tests --- src/types/multipoint.rs | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/types/multipoint.rs b/src/types/multipoint.rs index 3858fb3..e0dc8bd 100644 --- a/src/types/multipoint.rs +++ b/src/types/multipoint.rs @@ -83,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.item { + Geometry::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.item { + Geometry::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(); From 228d782b92a1fe28667e142a16ccf332834e7152 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 17 Jul 2024 16:06:15 -0400 Subject: [PATCH 04/10] add to changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index dacffce..1945a56 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 From c37f220850292f841ae56d49edc9145ea16f705a Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 17 Jul 2024 19:12:29 -0400 Subject: [PATCH 05/10] fix test --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index c28e3cb..688d476 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -284,6 +284,7 @@ fn infer_geom_dimension( tokens.next().unwrap().unwrap(); Ok(Dimension::XYZM) } + "EMPTY" => Ok(Dimension::XY), _ => Err("Unexpected word before open paren"), }, // Not a word, e.g. an open paren From 2aa5b188321fe27a48dd264212e54bd04d4e9ac1 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 17 Jul 2024 16:54:21 -0700 Subject: [PATCH 06/10] cargo fmt --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2bca30f..44ffe48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -432,7 +432,7 @@ mod tests { .unwrap(); match wkt { Wkt::Point(Point(Some(coord))) => { - assert_eq!(coord.x, 10.0); + 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)); From b1c5ad3f810c05cd99a4b09daab0a61206be3b49 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 17 Jul 2024 23:04:08 -0400 Subject: [PATCH 07/10] change to pub(crate) --- src/types/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index 097d7f9..b0dfc87 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -13,7 +13,7 @@ // limitations under the License. pub use self::coord::Coord; -pub use self::dimension::Dimension; +pub(crate) use self::dimension::Dimension; pub use self::geometrycollection::GeometryCollection; pub use self::linestring::LineString; pub use self::multilinestring::MultiLineString; From d6ff1e42eb38317af6a76822b7f347b7d5a5f0d0 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 17 Jul 2024 23:06:49 -0400 Subject: [PATCH 08/10] use eq_ignore_ascii_case --- src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 44ffe48..7262e12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -249,21 +249,21 @@ fn infer_geom_dimension( 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(x) => match x.as_str() { - "Z" => { + Token::Word(w) => match w.as_str() { + w if w.eq_ignore_ascii_case("Z") => { tokens.next().unwrap().unwrap(); Ok(Dimension::XYZ) } - "M" => { + w if w.eq_ignore_ascii_case("M") => { tokens.next().unwrap().unwrap(); Ok(Dimension::XYM) } - "ZM" => { + w if w.eq_ignore_ascii_case("ZM") => { tokens.next().unwrap().unwrap(); Ok(Dimension::XYZM) } - "EMPTY" => Ok(Dimension::XY), + 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 From bdaa0bc192a2ca370ebb9d5fc05cc016958d76e9 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 17 Jul 2024 23:07:50 -0400 Subject: [PATCH 09/10] condense import --- src/lib.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7262e12..2bbc41e 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::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}; +use crate::types::{ + Dimension, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, + Polygon, +}; mod to_wkt; mod tokenizer; From 225f1a1155c9ccb554248c79627091c8938f4b64 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 18 Jul 2024 15:25:03 -0400 Subject: [PATCH 10/10] Add no-whitespace support --- src/lib.rs | 178 ++++++++++++++++++++++++++++++++++++++-- src/types/linestring.rs | 22 +++++ src/types/point.rs | 13 +++ 3 files changed, 204 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2bbc41e..2b97b13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,33 +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_header(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_header( + tokens, + Some(Dimension::XYZ), + ); + x.map(|y| y.into()) + } + w if w.eq_ignore_ascii_case("POINTM") => { + 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_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_header(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_header(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_header(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_header(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_header(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_header(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"), @@ -279,8 +432,15 @@ where /// 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) -> Result { - let dim = infer_geom_dimension(tokens)?; + 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) } diff --git a/src/types/linestring.rs b/src/types/linestring.rs index 54ed424..ff84bcf 100644 --- a/src/types/linestring.rs +++ b/src/types/linestring.rs @@ -153,6 +153,28 @@ mod tests { 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/point.rs b/src/types/point.rs index 92e448b..a4d6123 100644 --- a/src/types/point.rs +++ b/src/types/point.rs @@ -98,6 +98,19 @@ mod tests { 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")