Repository: developmentseed/cql2-rs (language: python)

Issue:
Handle 3D points without the `Z`
From [the spec](https://docs.ogc.org/is/21-065r2/21-065r2.html):

> OGC WKT uses a "Z" character to distinguish the dimensionality of the coordinate reference system (CRS), e.g. POINT(7 51) for a 2D CRS and POINT Z(7 51 100) for a 3D CRS. In CQL2 Text, processors are required to be more tolerant and to determine the coordinate dimension from the coordinates.

Great job folks, you relaxed your own requirement 🤦🏼. But this means we probably need to either:

- Push tolerant parsing down to https://github.com/georust/wkt, or
- Implement our own

Seems better to push tolerate parsing down, I reckon.
Check and enable Z, M, and ZM
## What I am changing

- Enable and check Z, M, and ZM handling for WKT

## How I did it

- Added our own `Geometry` enum which holds the original geometry in its "incoming" format, and only converts to the other format on serialization / `to_text` calls
- Fixed the parse grammar to handle `ZM` (it had to come first)

## Related Issues

- Closes #14 



Discussion / hints:
<sigh/>

I'd rather depend on another library, but rolling our own for parsing from CQL2-Text would be fairly straight forward as we already have all the plumbing in place. Mostly would need to adjust the cql2.pest a little bit.


Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index c7f905d..1eeba66 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,7 +10,7 @@ geojson = "0.24.1"
 geozero = "0.14.0"
 lazy_static = "1.5"
 pest = "2.7"
-pest_derive = "2.7"
+pest_derive = { version = "2.7", features = ["grammar-extras"] }
 serde = "1.0"
 serde_derive = "1.0"
 serde_json = { version = "1.0", features = ["preserve_order"] }
diff --git a/src/cql2.pest b/src/cql2.pest
index 13a1d98..8f22257 100644
--- a/src/cql2.pest
+++ b/src/cql2.pest
@@ -27,32 +27,32 @@ TIMESTAMP_STR = { DATE_STR ~ ("T" | " ") ~ TIME_STR }
 TORD = { QUOTE ~ (TIMESTAMP_STR | DATE_STR) ~ QUOTE }
 
 // wkt
-PADDED_DECIMAL     = _{ WHITESPACE* ~ DECIMAL* ~ WHITESPACE* }
-COORD              = _{ PADDED_DECIMAL{1, 4} }
-PCOORD             = _{ WHITESPACE* ~ LPAREN ~ COORD ~ RPAREN ~ WHITESPACE* }
-COORDLIST          = _{ WHITESPACE* ~ COORD ~ (COMMADELIM ~ COORD)* ~ WHITESPACE* }
-PCOORDLIST         = _{ WHITESPACE* ~ LPAREN ~ COORDLIST ~ RPAREN ~ WHITESPACE* }
-PCOORDLISTLIST     = _{ WHITESPACE* ~ LPAREN ~ PCOORDLIST ~ (COMMADELIM ~ PCOORDLIST)* ~ RPAREN ~ WHITESPACE* }
-PCOORDLISTLISTLIST = _{ WHITESPACE* ~ LPAREN ~ PCOORDLISTLIST ~ (COMMADELIM ~ PCOORDLISTLIST)* ~ RPAREN ~ WHITESPACE* }
+PADDED_DECIMAL     = { WHITESPACE* ~ DECIMAL ~ WHITESPACE* }
+COORD              = { #four_d = PADDED_DECIMAL{4} | #three_d = PADDED_DECIMAL{3} | #two_d = PADDED_DECIMAL{2} }
+PCOORD             = { WHITESPACE* ~ LPAREN ~ COORD ~ RPAREN ~ WHITESPACE* }
+COORDLIST          = { WHITESPACE* ~ COORD ~ (COMMADELIM ~ COORD)* ~ WHITESPACE* }
+PCOORDLIST         = { WHITESPACE* ~ LPAREN ~ COORDLIST ~ RPAREN ~ WHITESPACE* }
+PCOORDLISTLIST     = { WHITESPACE* ~ LPAREN ~ PCOORDLIST ~ (COMMADELIM ~ PCOORDLIST)* ~ RPAREN ~ WHITESPACE* }
+PCOORDLISTLISTLIST = { WHITESPACE* ~ LPAREN ~ PCOORDLISTLIST ~ (COMMADELIM ~ PCOORDLISTLIST)* ~ RPAREN ~ WHITESPACE* }
 
-ZM = _{ WHITESPACE* ~ (^"ZM" | ^"Z" | ^"M")? ~ WHITESPACE* }
+ZM = { WHITESPACE* ~ (^"ZM" | ^"Z" | ^"M")? ~ WHITESPACE* }
 
-POINT      = @{ ^"POINT" ~ ZM ~ PCOORD }
-LINESTRING = @{ ^"LINESTRING" ~ ZM ~ PCOORDLIST }
-POLYGON    = @{ ^"POLYGON" ~ ZM ~ PCOORDLISTLIST }
+POINT      = ${ ^"POINT" ~ ZM ~ PCOORD }
+LINESTRING = ${ ^"LINESTRING" ~ ZM ~ PCOORDLIST }
+POLYGON    = ${ ^"POLYGON" ~ ZM ~ PCOORDLISTLIST }
 
-MULTIPOINT_1 = _{ ^"MULTIPOINT" ~ ZM ~ PCOORDLIST }
-MULTIPOINT_2 = _{ ^"MULTIPOINT" ~ ZM ~ PCOORDLISTLIST }
-MULTIPOINT   = @{ MULTIPOINT_1 | MULTIPOINT_2 }
+MULTIPOINT_1 = ${ ^"MULTIPOINT" ~ ZM ~ PCOORDLIST }
+MULTIPOINT_2 = ${ ^"MULTIPOINT" ~ ZM ~ PCOORDLISTLIST }
+MULTIPOINT   = ${ MULTIPOINT_1 | MULTIPOINT_2 }
 
-MULTILINESTRING = @{ ^"MULTILINESTRING" ~ ZM ~ PCOORDLISTLIST }
-MULTIPOLYGON    = @{ ^"MULTIPOLYGON" ~ ZM ~ PCOORDLISTLISTLIST }
+MULTILINESTRING = ${ ^"MULTILINESTRING" ~ ZM ~ PCOORDLISTLIST }
+MULTIPOLYGON    = ${ ^"MULTIPOLYGON" ~ ZM ~ PCOORDLISTLISTLIST }
 
-GEOMETRY_SINGLE = _{ WHITESPACE* ~ (POINT | LINESTRING | POLYGON | MULTIPOINT | MULTILINESTRING | MULTIPOLYGON) ~ WHITESPACE* }
+GEOMETRY_SINGLE = ${ WHITESPACE* ~ (POINT | LINESTRING | POLYGON | MULTIPOINT | MULTILINESTRING | MULTIPOLYGON) ~ WHITESPACE* }
 
-GEOMETRY_COLLECTION = @{ ^"GEOMETRYCOLLECTION" ~ WHITESPACE* ~ LPAREN ~ GEOMETRY_SINGLE ~ (COMMADELIM ~ GEOMETRY_SINGLE)* ~ RPAREN }
+GEOMETRY_COLLECTION = ${ ^"GEOMETRYCOLLECTION" ~ WHITESPACE* ~ LPAREN ~ GEOMETRY_SINGLE ~ (COMMADELIM ~ GEOMETRY_SINGLE)* ~ RPAREN }
 
-GEOMETRY = @{ GEOMETRY_SINGLE | GEOMETRY_COLLECTION }
+GEOMETRY = ${ GEOMETRY_SINGLE | GEOMETRY_COLLECTION }
 
 IdentifierInner = _{
     ALPHABETIC ~ (ALPHABETIC | NUMBER | UNDERSCORE | PERIOD | COLON)*
diff --git a/src/expr.rs b/src/expr.rs
index b5bb74b..ef6bc86 100644
--- a/src/expr.rs
+++ b/src/expr.rs
@@ -281,7 +281,6 @@ impl FromStr for Expr {
         }
     }
 }
-
 #[cfg(test)]
 mod tests {
     use super::Expr;
@@ -292,6 +291,12 @@ mod tests {
         assert_eq!("POINT Z(-105.1019 40.1672 4981)", point.to_text().unwrap());
     }
 
+    #[test]
+    fn implicit_z() {
+        let point: Expr = "POINT (-105.1019 40.1672 4981)".parse().unwrap();
+        assert_eq!("POINT Z(-105.1019 40.1672 4981)", point.to_text().unwrap());
+    }
+
     #[test]
     fn keep_m() {
         let point: Expr = "POINT M(-105.1019 40.1672 42)".parse().unwrap();
diff --git a/src/parser.rs b/src/parser.rs
index 5022730..e46639e 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -102,7 +102,30 @@ fn parse_expr(expression_pairs: Pairs<'_, Rule>) -> Result<Expr, Error> {
             Rule::Identifier => Ok(Expr::Property {
                 property: strip_quotes(primary.as_str()).to_string(),
             }),
-            Rule::GEOMETRY => Ok(Expr::Geometry(Geometry::Wkt(primary.as_str().to_string()))),
+            Rule::GEOMETRY => {
+                // These are some incredibly annoying backflips to handle
+                // geometries without `Z` but that have 3D coordinates. It's
+                // not part of OGC WKT, but CQL2 demands 🤦‍♀️.
+                let start = primary.as_span().start();
+                let s = primary.as_str().to_string();
+                let pairs = primary.into_inner();
+                if pairs.find_first_tagged("three_d").is_some() {
+                    let zm = pairs
+                        .flatten()
+                        .find(|pair| matches!(pair.as_rule(), Rule::ZM))
+                        .expect("all geometries should have a ZM rule");
+                    if zm.as_str().chars().all(|c| c.is_ascii_whitespace()) {
+                        let span = zm.as_span();
+                        let s = format!(
+                            "{} Z{}",
+                            &s[0..span.start() - start],
+                            &s[span.end() - start..]
+                        );
+                        return Ok(Expr::Geometry(Geometry::Wkt(s)));
+                    }
+                }
+                Ok(Expr::Geometry(Geometry::Wkt(s)))
+            }
             Rule::Function => {
                 let mut pairs = primary.into_inner();
                 let op = strip_quotes(


Test patch:
diff --git a/tests/README.md b/tests/README.md
index bd17477..d996e2b 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,7 +1,11 @@
-# Expected test output
+# Tests
+
+The `fixtures` directory is copied directly from <https://github.com/opengeospatial/ogcapi-features/tree/cql2-1.0.0/cql2/standard/schema/examples>.
+
+## Expected test output
 
 To generate:
 
 ```shell
-tests/expected/generate
+tests/generate-expected
 ```
diff --git a/tests/fixtures/text/example49-alt01.txt b/tests/fixtures/text/example49-alt01.txt
index 434a77b..91913ee 100644
--- a/tests/fixtures/text/example49-alt01.txt
+++ b/tests/fixtures/text/example49-alt01.txt
@@ -1,1 +1,1 @@
-S_WITHIN(POLYGON Z((-49.88024 0.5 -75993.341684, -1.5 -0.99999 -100000.0, 0.0 0.5 -0.333333, -49.88024 0.5 -75993.341684), (-65.887123 2.00001 -100000.0, 0.333333 -53.017711 -79471.332949, 180.0 0.0 1852.616704, -65.887123 2.00001 -100000.0)), "geometry")
+S_WITHIN(POLYGON ((-49.88024 0.5 -75993.341684, -1.5 -0.99999 -100000.0, 0.0 0.5 -0.333333, -49.88024 0.5 -75993.341684), (-65.887123 2.00001 -100000.0, 0.333333 -53.017711 -79471.332949, 180.0 0.0 1852.616704, -65.887123 2.00001 -100000.0)), "geometry")