georust / wkt

Rust read/write support for well-known text (WKT)

Home Page:https://crates.io/crates/wkt

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Use new TZM geo-types instead the custom WKT ones

nyurik opened this issue · comments

Once (if) the new geo-types with 3dM support is merged, I think wkt can migrate to use them directly, instead of having its own duplicate set of types. This should improve performance (no extra transformation), and simplify code - one set of types for everything.

// current WKT type
pub struct Coord<T: WktFloat> {
    pub x: T,
    pub y: T,
    pub z: Option<T>,
    pub m: Option<T>,
}

// corresponding geo type
pub struct Coordinate<T: CoordNum, Z: ZCoord, M: Measure> {
    pub x: T,
    pub y: T,
    pub z: Z,
    pub m: M,
}

The 3dM types require proper generics (either concrete float type or a NoValue empty struct). We could represent it with a new Wkt enum instead of the existing pub struct Wkt<T> { pub item: Geometry<T> }. NoValue might need to be updated to support all CoordNum requirements.

// Usage:
match Wkt::from_str("LINESTRING (10 -20, -0 -0.5)").unwrap() {
  Wkt(g) => ..., // 2D value
  WktM(g) => ..., // 2D+M value
  Wkt3D(g) => ..., // 3D value
  Wkt3DM(g) => ..., // 3D+M value
  WktEmpty(g) => ..., // Empty value, e.g. "POINT EMPTY"
}

// Note that all T, Z, and M types of the 3dM geo-types in WKT are used as the same `T` generic type, except for the empty type
pub enum Wkt {
    Wkt(Geometry<T>),
    WktM(GeometryM<T>),
    Wkt3D(Geometry3D<T>),
    Wkt3DM(Geometry3DM<T>),
    WktEmpty(Geometry<NoValue>),
}

One point of friction is POINT EMPTY. It's not clear how to handle that with geo-types.

I think we can simply add another enum for that, treating an empty point, 2d point, and 3d point as separate cases. The only issue I see is if wkt has multiple types of points in the same geometry

Can you spell that out for me a bit more?

What would this deserialize to?

let empty_point: geo_types::Geometry = Wkt::from_str("POINT EMPTY");
let empty_point: geo_types::Geometry = Wkt::from_str("POINT EMPTY");

geo_types::Geometry<T,Z,M> would need to use a NoValue for all 3 generic params. That won't work with the proposed geotypes implementation because T has stricter requirements than Z and M -- T requires CoordNum. See related discussion.

To support empty types in WKT, we could either implement ToPrimitive, NumCast, and Num for the NoValue in geotypes (as the linked comment suggests), or we could let WKT implement its own version of NoValue with all CoordNum traits implemented. In either case, we would end up with this:

if let WktEmpty(g) = Wkt::from_str("POINT EMPTY").unwrap() {
   // g is a geo_types::Geometry<NoValue>(geo_types::Point<NoValue>(...))
}

Hmm... I'm not sure I understand your suggestion.

To dig in a bit, I kind of get how geo_types::Point<NoValue, NoValue, NoValue> could maybe be thought of as a corollary to POINT EMPTY.

But we need to know what all three of the generic params are before we start parsing.

let geom: Geometry<f32, NoValue, NoValue> = Wkt::from_str(str).unwrap();

We can't say both simultaneously:

// this if `str` == `POINT EMPTY`
let geom: Geometry<NoValue, NoValue, NoValue> = Wkt::from_str(str).unwrap();

// but otherwise this if `str` is a normal geometry
let geom: Geometry<f32, NoValue, NoValue> = Wkt::from_str(str).unwrap();

I think you understand that - I'm just not sure the entirety of what you're proposing. Is it a bigger api change, something like this?:

enum WktGeometryOrEmptyPoint {
   EmptyPoint,
   Geometry(Wkt)
}

Wkt::from_str(str) -> WktGeometryOrEmptyPoint

@michaelkirk you are correct - I am proposing a slightly bigger parsing API change - an enum wrapping generic typing variation. Naming is TBD.

pub enum Wkt<T: CoordNum> {
    Empty(Geometry<NoValue>),
    Geo2D(Geometry<T>),
    Geo2DM(Geometry<T, NoValue, T>),
    Geo3D(Geometry<T, T>),
    Geo3DM(Geometry<T, T, T>),
}

// This declaration would be identical to what it is now (slightly changed here for viewing)
impl<T: CoordNum + FromStr + Default> FromStr for Wkt<T> {
    type Err = &'static str;

    fn from_str(wkt_str: &str) -> Result<Self, Self::Err> {
        todo!()
    }
}
let g: Wkt = Wkt::from_str("GEOMETRYCOLLECTION(LINESTRING(10 20,20 30), LINESTRING EMPTY)")

What member of the Wkt enum would this be?

That's a good question - would this be a legal WKT and/or do we want to support such cases?

One (bad?) option could be some sort of a boxing variant, e.g. in case of mixed type. I am not a Rust expert just yet, so this is outside of my expertise/comfort zone for now.

pub enum Wkt<T: CoordNum> {
    Empty(Geometry<NoValue>),
    Geo2D(Geometry<T>),
    Geo2DM(Geometry<T, NoValue, T>),
    Geo3D(Geometry<T, T>),
    Geo3DM(Geometry<T, T, T>),
    Boxed(<magic>),  // <-- some sort of a trait-based boxed value?
}

That's a good question - would this be a legal WKT and/or do we want to support such cases?

Whoops, that example I gave was invalid WKT. I forgot that MultiLineStrings don't tag each individual LINESTRING...

I've updated it with a corollary that we do support today:

use crate::TryFromWkt;    
let g = geo_types::GeometryCollection::<f64>::try_from_wkt_str("GEOMETRYCOLLECTION(LINESTRING(10 20,20 30), LINESTRING EMPTY)").unwrap();    
println!(">>>>>: {:?}", g);  
>>>>> GeometryCollection([LineString(LineString([Coordinate { x: 10.0, y: 20.0 }, Coordinate { x: 20.0, y: 30.0 }])), LineString(LineString([]))])

It's kind of an edge case, but I do think these kinds of degenerate geometries exist in the wild, and that it's nice to support them where reasonable.

I guess my point is that it really seems like POINT EMPTY is a special beast in that it can't be expressed as a geo-type today. Any work-around might benefit from very narrowly only accounting for POINT EMPTY rather than devising some system for "all empty geometries" which, I think can be reasonably handled today.