diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b08096b6f..52f0e119a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2204,6 +2204,15 @@ dependencies = [ "sif-itree", ] +[[package]] +name = "geo-traits" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7c353d12a704ccfab1ba8bfb1a7fe6cb18b665bf89d37f4f7890edcd260206" +dependencies = [ + "geo-types", +] + [[package]] name = "geo-types" version = "0.7.19" @@ -3572,6 +3581,7 @@ dependencies = [ "flate2", "fsst-rs", "geo", + "geo-traits", "geo-types", "hex", "hilbert_2d", @@ -3593,6 +3603,7 @@ dependencies = [ "union-find", "usize_cast", "wide", + "wkt", "zigzag", "zstd", ] @@ -7836,6 +7847,19 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wkt" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb2b923ccc882312e559ffaa832a055ba9d1ac0cc8e86b3e25453247e4b81d7" +dependencies = [ + "geo-traits", + "geo-types", + "log", + "num-traits", + "thiserror 1.0.69", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 12b5c8845..37dc64af6 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -37,6 +37,7 @@ flate2 = "1" fsst-rs = "0.5" futures = "0.3" geo = { version = "0.33.1", default-features = false } +geo-traits = { version = "0.3.0", features = ["geo-types"] } geo-types = "0.7.19" glob = "0.3" globset = "0.4" @@ -84,6 +85,7 @@ usize_cast = "1.1.0" walkdir = "2" wasm-bindgen = "0.2" wide = "1.5.0" +wkt = "0.14.0" xxhash-rust = { version = "0.8", features = ["xxh3"] } zigzag = "0.1.0" zstd = "0.13" diff --git a/rust/mlt-core/Cargo.toml b/rust/mlt-core/Cargo.toml index 2bff5e670..1b4494b2f 100644 --- a/rust/mlt-core/Cargo.toml +++ b/rust/mlt-core/Cargo.toml @@ -52,6 +52,7 @@ fast-mvt.workspace = true fastpfor.workspace = true fsst-rs.workspace = true geo = { workspace = true, features = ["earcut"] } +geo-traits.workspace = true geo-types.workspace = true hex.workspace = true hilbert_2d.workspace = true @@ -67,6 +68,7 @@ thiserror.workspace = true union-find.workspace = true usize_cast.workspace = true wide.workspace = true +wkt.workspace = true zigzag.workspace = true [dev-dependencies] diff --git a/rust/mlt-core/src/codecs/zigzag.rs b/rust/mlt-core/src/codecs/zigzag.rs index 2f0c59ee1..d7b4ebb86 100644 --- a/rust/mlt-core/src/codecs/zigzag.rs +++ b/rust/mlt-core/src/codecs/zigzag.rs @@ -4,6 +4,10 @@ use zigzag::ZigZag; use crate::MltError::InvalidPairStreamSize; use crate::{Decoder, MltResult}; +/// Upper bound on vertex components for the componentwise-delta codecs (X, Y, Z, M). Lets the +/// per-component running state live in a small stack array instead of a per-call heap `Vec`. +const MAX_COMPONENTS: usize = 4; + /// ZigZag-encode `data` into `target`. /// /// `target` is treated as a scratch buffer: cleared before writing. @@ -31,15 +35,17 @@ pub fn encode_zigzag_delta<'a, T: Copy + ZigZag + WrappingSub>( target } -/// Encode signed integer vec2 values using componentwise delta + zigzag into `target`. +/// Encode interleaved `n_dims`-component vertices using componentwise delta + zigzag into `target`. /// -/// Input: `[x0, y0, x1, y1, ...]` +/// Input (`n_dims = 2`): `[x0, y0, x1, y1, ...]` /// Output: `[zigzag(x0-0), zigzag(y0-0), zigzag(x1-x0), zigzag(y1-y0), ...]` /// -/// `target` is treated as a scratch buffer: cleared before writing. -/// This is the inverse of `decode_componentwise_delta_vec2s`. -pub fn encode_componentwise_delta_vec2s<'a, T>( +/// The delta runs independently per component position, so `n_dims` must match the value used +/// when decoding. `target` is treated as a scratch buffer: cleared before writing. +/// This is the inverse of `decode_componentwise_delta`. +pub fn encode_componentwise_delta<'a, T>( data: &[T], + n_dims: usize, target: &'a mut Vec, ) -> &'a [T::UInt] where @@ -47,13 +53,13 @@ where { target.clear(); target.reserve(data.len()); - let mut prev_x = T::zero(); - let mut prev_y = T::zero(); - for chunk in data.chunks_exact(2) { - let (x, y) = (chunk[0], chunk[1]); - target.push(T::encode(x.wrapping_sub(&prev_x))); - target.push(T::encode(y.wrapping_sub(&prev_y))); - (prev_x, prev_y) = (x, y); + debug_assert!(n_dims <= MAX_COMPONENTS); + let mut prev = [T::zero(); MAX_COMPONENTS]; + for chunk in data.chunks_exact(n_dims) { + for (j, &v) in chunk.iter().enumerate() { + target.push(T::encode(v.wrapping_sub(&prev[j]))); + prev[j] = v; + } } target } @@ -79,26 +85,28 @@ pub fn decode_zigzag_delta, U: ' .collect()) } -/// Decode ([`ZigZag`] + delta) for Vec2s, charging `dec` for the output allocation. -// TODO: The encoded process is (delta + ZigZag) for each component -pub fn decode_componentwise_delta_vec2s( +/// Decode ([`ZigZag`] + delta) for interleaved `n_dims`-component vertices, charging `dec` for +/// the output allocation. The delta is reconstructed independently per component position, so +/// `n_dims` must match the value used when encoding. Inverse of [`encode_componentwise_delta`]. +pub fn decode_componentwise_delta( data: &[T::UInt], + n_dims: usize, dec: &mut Decoder, ) -> MltResult> { - if data.is_empty() || !data.len().is_multiple_of(2) { + if data.is_empty() || n_dims == 0 || !data.len().is_multiple_of(n_dims) { return Err(InvalidPairStreamSize(data.len())); } let alloc_size = data.len(); let mut result = dec.alloc(alloc_size)?; - let mut last1 = T::zero(); - let mut last2 = T::zero(); - - for i in (0..data.len()).step_by(2) { - last1 = last1.wrapping_add(&T::decode(data[i])); - last2 = last2.wrapping_add(&T::decode(data[i + 1])); - result.push(last1); - result.push(last2); + debug_assert!(n_dims <= MAX_COMPONENTS); + let mut last = [T::zero(); MAX_COMPONENTS]; + + for chunk in data.chunks_exact(n_dims) { + for (j, &v) in chunk.iter().enumerate() { + last[j] = last[j].wrapping_add(&T::decode(v)); + result.push(last[j]); + } } dec.adjust_alloc(&result, alloc_size)?; @@ -141,8 +149,8 @@ mod tests { &data[..data.len() - 1] }; let mut encoded = Vec::new(); - let data = encode_componentwise_delta_vec2s(data_slice, &mut encoded); - let decoded = decode_componentwise_delta_vec2s::(data, &mut dec()).unwrap(); + let data = encode_componentwise_delta(data_slice, 2, &mut encoded); + let decoded = decode_componentwise_delta::(data, 2, &mut dec()).unwrap(); prop_assert_eq!(data_slice, &decoded); } } @@ -193,7 +201,16 @@ mod tests { #[test] fn test_decode_componentwise_delta_vec2s() { let values = &[1_u32, 2, 3, 4]; - let decoded = decode_componentwise_delta_vec2s::(values, &mut dec()).unwrap(); + let decoded = decode_componentwise_delta::(values, 2, &mut dec()).unwrap(); assert_eq!(&decoded, &[-1_i32, 1, -3, 3]); } + + #[test] + fn test_componentwise_delta_vec3_roundtrip() { + let data = [10_i32, 20, 30, 11, 19, 35, 5, 5, 5]; + let mut encoded = Vec::new(); + let enc = encode_componentwise_delta(&data, 3, &mut encoded); + let decoded = decode_componentwise_delta::(enc, 3, &mut dec()).unwrap(); + assert_eq!(&data[..], &decoded); + } } diff --git a/rust/mlt-core/src/convert/geojson.rs b/rust/mlt-core/src/convert/geojson.rs index 17856d713..c31198f54 100644 --- a/rust/mlt-core/src/convert/geojson.rs +++ b/rust/mlt-core/src/convert/geojson.rs @@ -3,7 +3,6 @@ use std::collections::BTreeMap; use std::str::FromStr; -use geo_types::Geometry; use serde::ser::SerializeMap as _; use serde::{Deserialize, Serialize}; use serde_json::{Number, Value}; @@ -72,7 +71,7 @@ impl FromStr for FeatureCollection { #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct Feature { #[serde(with = "geom_serde")] - pub geometry: Geometry, + pub geometry: wkt::Wkt, #[serde(default, skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(default)] @@ -81,7 +80,7 @@ pub struct Feature { pub ty: String, } -struct Geom32Wire<'a>(&'a Geometry); +struct Geom32Wire<'a>(&'a wkt::Wkt); impl Serialize for Geom32Wire<'_> { fn serialize(&self, s: S) -> Result { geom_serde::serialize(self.0, s) @@ -103,67 +102,103 @@ impl Serialize for Feature { } } -/// Serialize/deserialize [`Geometry`](geo_types::Geometry) in `GeoJSON` wire format: -/// `{"type":"…","coordinates":…}` with `[x, y]` integer arrays. +/// Serialize/deserialize [`wkt::Wkt`] in `GeoJSON` wire format: +/// `{"type":"…","coordinates":…}`. +/// +/// Each coordinate is a `[x, y]` array, or `[x, y, z]` when the geometry carries a Z (3D). +/// The container [`Dimension`] is inferred from the first coordinate on deserialize. mod geom_serde { - use geo_types::{ - Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, - }; use serde::de::Error as _; use serde::ser::{Error, SerializeMap as _}; use serde::{Deserialize, Deserializer, Serializer}; use serde_json::Value; + use wkt::Wkt; + use wkt::types::{ + Coord, Dimension, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, + }; - type Arr = [i32; 2]; + /// A coordinate as a 2- or 3-element array (`[x, y]` or `[x, y, z]`). + type Arr = Vec; + + fn coord_arr(c: &Coord) -> Arr { + match c.z { + Some(z) => vec![c.x, c.y, z], + None => vec![c.x, c.y], + } + } + + fn point_arr(p: &Point) -> Option { + p.coord().map(coord_arr) + } fn ls_arr(ls: &LineString) -> Vec { - ls.0.iter().copied().map(Into::into).collect() + ls.coords().iter().map(coord_arr).collect() } fn poly_arr(poly: &Polygon) -> Vec> { - std::iter::once(poly.exterior()) - .chain(poly.interiors()) - .map(ls_arr) - .collect() + poly.rings().iter().map(ls_arr).collect() + } + + fn arr_coord(a: &Arr) -> Coord { + Coord { + x: a[0], + y: a[1], + z: a.get(2).copied(), + m: None, + } + } + + /// Infer the [`Dimension`] of a coordinate sequence from its first coordinate. + fn dim_of(coords: &[Coord]) -> Dimension { + coords.first().map_or(Dimension::XY, Coord::dimension) } fn arr_ls(v: Vec) -> LineString { - LineString::from(v) + let coords: Vec> = v.into_iter().map(|a| arr_coord(&a)).collect(); + let dim = dim_of(&coords); + LineString::new(coords, dim) } fn arr_poly(rings: Vec>) -> Polygon { - let mut it = rings.into_iter(); - let ext = it.next().map_or_else(|| LineString(vec![]), arr_ls); - Polygon::new(ext, it.map(arr_ls).collect()) + let rings: Vec> = rings.into_iter().map(arr_ls).collect(); + let dim = rings.first().map_or(Dimension::XY, LineString::dimension); + Polygon::new(rings, dim) } - pub fn serialize(g: &Geometry, s: S) -> Result { + pub fn serialize(g: &Wkt, s: S) -> Result { let mut m = s.serialize_map(Some(2))?; let (ty, coords): (&str, Value) = match g { - Geometry::Point(p) => ("Point", serde_json::to_value(Arr::from(*p)).unwrap()), - Geometry::LineString(ls) => ("LineString", serde_json::to_value(ls_arr(ls)).unwrap()), - Geometry::Polygon(poly) => ("Polygon", serde_json::to_value(poly_arr(poly)).unwrap()), - Geometry::MultiPoint(mp) => ( + Wkt::Point(p) => ( + "Point", + serde_json::to_value(point_arr(p).unwrap_or_default()).unwrap(), + ), + Wkt::LineString(ls) => ("LineString", serde_json::to_value(ls_arr(ls)).unwrap()), + Wkt::Polygon(poly) => ("Polygon", serde_json::to_value(poly_arr(poly)).unwrap()), + Wkt::MultiPoint(mp) => ( "MultiPoint", - serde_json::to_value(mp.0.iter().copied().map(Arr::from).collect::>()) + serde_json::to_value(mp.points().iter().filter_map(point_arr).collect::>()) .unwrap(), ), - Geometry::MultiLineString(mls) => ( + Wkt::MultiLineString(mls) => ( "MultiLineString", - serde_json::to_value(mls.iter().map(ls_arr).collect::>()).unwrap(), + serde_json::to_value(mls.line_strings().iter().map(ls_arr).collect::>()) + .unwrap(), ), - Geometry::MultiPolygon(mpoly) => ( + Wkt::MultiPolygon(mpoly) => ( "MultiPolygon", - serde_json::to_value(mpoly.iter().map(poly_arr).collect::>()).unwrap(), + serde_json::to_value(mpoly.polygons().iter().map(poly_arr).collect::>()) + .unwrap(), ), - _ => return Err(Error::custom("unsupported geometry variant")), + Wkt::GeometryCollection(_) => { + return Err(Error::custom("unsupported geometry variant")); + } }; m.serialize_entry("type", ty)?; m.serialize_entry("coordinates", &coords)?; m.end() } - pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { fn parse(v: Value) -> Result { serde_json::from_value(v).map_err(E::custom) } @@ -177,20 +212,27 @@ mod geom_serde { let Wire { ty, coordinates: c } = Wire::deserialize(d)?; Ok(match ty.as_str() { - "Point" => Geometry::Point(Point::from(parse::(c)?)), - "LineString" => Geometry::LineString(arr_ls(parse(c)?)), - "Polygon" => Geometry::Polygon(arr_poly(parse(c)?)), + "Point" => Wkt::Point(Point::from_coord(arr_coord(&parse::(c)?))), + "LineString" => Wkt::LineString(arr_ls(parse(c)?)), + "Polygon" => Wkt::Polygon(arr_poly(parse(c)?)), "MultiPoint" => { let v: Vec = parse(c)?; - Geometry::MultiPoint(MultiPoint(v.into_iter().map(Point::from).collect())) + let points: Vec> = + v.iter().map(|a| Point::from_coord(arr_coord(a))).collect(); + let dim = points.first().map_or(Dimension::XY, Point::dimension); + Wkt::MultiPoint(MultiPoint::new(points, dim)) } "MultiLineString" => { let v: Vec> = parse(c)?; - Geometry::MultiLineString(MultiLineString(v.into_iter().map(arr_ls).collect())) + let lines: Vec> = v.into_iter().map(arr_ls).collect(); + let dim = lines.first().map_or(Dimension::XY, LineString::dimension); + Wkt::MultiLineString(MultiLineString::new(lines, dim)) } "MultiPolygon" => { let v: Vec>> = parse(c)?; - Geometry::MultiPolygon(MultiPolygon(v.into_iter().map(arr_poly).collect())) + let polys: Vec> = v.into_iter().map(arr_poly).collect(); + let dim = polys.first().map_or(Dimension::XY, Polygon::dimension); + Wkt::MultiPolygon(MultiPolygon::new(polys, dim)) } _ => { return Err(D::Error::unknown_variant( @@ -310,3 +352,59 @@ fn json_values_equal(a: &Value, b: &Value) -> bool { _ => a == b, } } + +#[cfg(test)] +mod tests { + use wkt::Wkt; + use wkt::types::{Coord, Dimension, LineString, Point}; + + use super::*; + + fn feature(geometry: Wkt) -> Feature { + Feature { + geometry, + id: None, + properties: BTreeMap::new(), + ty: "Feature".into(), + } + } + + #[test] + fn linestring_3d_serde_roundtrip() { + let geom = Wkt::LineString(LineString::new( + vec![ + Coord { + x: 1, + y: 2, + z: Some(3), + m: None, + }, + Coord { + x: 4, + y: 5, + z: Some(6), + m: None, + }, + ], + Dimension::XYZ, + )); + let json = serde_json::to_string(&feature(geom.clone())).unwrap(); + assert!(json.contains("[1,2,3]"), "expected 3D coords in {json}"); + let back: Feature = serde_json::from_str(&json).unwrap(); + assert_eq!(back.geometry, geom); + } + + #[test] + fn point_2d_serde_has_no_z() { + let geom = Wkt::Point(Point::from_coord(Coord { + x: 1, + y: 2, + z: None, + m: None, + })); + let json = serde_json::to_string(&feature(geom.clone())).unwrap(); + assert!(json.contains("[1,2]"), "expected 2D coords in {json}"); + let back: Feature = serde_json::from_str(&json).unwrap(); + assert_eq!(back.geometry, geom); + } +} diff --git a/rust/mlt-core/src/convert/geom.rs b/rust/mlt-core/src/convert/geom.rs new file mode 100644 index 000000000..18f94da75 --- /dev/null +++ b/rust/mlt-core/src/convert/geom.rs @@ -0,0 +1,133 @@ +//! Conversions between geometry representations. +//! +//! MLT accepts any geometry that implements [`geo_traits::GeometryTrait`] as encoder input +//! (so both [`geo_types`] and [`wkt`] values work), and produces [`wkt::Wkt`] as decoder output +//! (the dimension-aware type that can carry an optional Z coordinate). +//! +//! [`to_wkt`] normalizes any geo-traits geometry into a [`wkt::Wkt`], preserving coordinate +//! dimensionality. [`geo_traits::to_geo::ToGeoGeometry::to_geometry`] handles the reverse +//! (`wkt::Wkt` -> `geo_types::Geometry`, dropping any Z) for the 2D MVT boundary. + +use geo_traits::{ + CoordTrait, Dimensions, GeometryCollectionTrait, GeometryTrait, GeometryType, LineStringTrait, + LineTrait, MultiLineStringTrait, MultiPointTrait, MultiPolygonTrait, PointTrait, PolygonTrait, + RectTrait, TriangleTrait, +}; +use wkt::Wkt; +use wkt::types::{ + Coord, Dimension, GeometryCollection, LineString, MultiLineString, MultiPoint, MultiPolygon, + Point, Polygon, +}; + +/// Map a geo-traits [`Dimensions`] to the wkt [`Dimension`] enum. +fn wkt_dim(dim: Dimensions) -> Dimension { + match dim { + Dimensions::Xyz | Dimensions::Unknown(3) => Dimension::XYZ, + Dimensions::Xym => Dimension::XYM, + Dimensions::Xyzm | Dimensions::Unknown(4..) => Dimension::XYZM, + Dimensions::Xy | Dimensions::Unknown(_) => Dimension::XY, + } +} + +/// Build a wkt [`Coord`] from any [`CoordTrait`], preserving Z / M according to its dimension. +fn coord_of>(c: &C) -> Coord { + let (z, m) = match c.dim() { + Dimensions::Xyz | Dimensions::Unknown(3) => (c.nth(2), None), + Dimensions::Xym => (None, c.nth(2)), + Dimensions::Xyzm | Dimensions::Unknown(4..) => (c.nth(2), c.nth(3)), + Dimensions::Xy | Dimensions::Unknown(_) => (None, None), + }; + Coord { + x: c.x(), + y: c.y(), + z, + m, + } +} + +fn point_of>(p: &P) -> Point { + let dim = wkt_dim(p.dim()); + Point::new(p.coord().map(|c| coord_of(&c)), dim) +} + +fn line_string_of>(ls: &L) -> LineString { + let dim = wkt_dim(ls.dim()); + let coords = ls.coords().map(|c| coord_of(&c)).collect(); + LineString::new(coords, dim) +} + +fn polygon_of>(poly: &P) -> Polygon { + let dim = wkt_dim(poly.dim()); + let rings = poly + .exterior() + .into_iter() + .chain(poly.interiors()) + .map(|r| line_string_of(&r)) + .collect(); + Polygon::new(rings, dim) +} + +/// Normalize any [`geo_traits::GeometryTrait`] geometry into a [`wkt::Wkt`], preserving +/// coordinate dimensionality. `Rect` and `Triangle` are converted to polygons and `Line` +/// to a line string, mirroring the encoder's handling of those geo-types-only variants. +pub(crate) fn to_wkt(geom: &impl GeometryTrait) -> Wkt { + match geom.as_type() { + GeometryType::Point(p) => Wkt::Point(point_of(p)), + GeometryType::LineString(ls) => Wkt::LineString(line_string_of(ls)), + GeometryType::Polygon(poly) => Wkt::Polygon(polygon_of(poly)), + GeometryType::MultiPoint(mp) => { + let dim = wkt_dim(mp.dim()); + let points = mp.points().map(|p| point_of(&p)).collect(); + Wkt::MultiPoint(MultiPoint::new(points, dim)) + } + GeometryType::MultiLineString(mls) => { + let dim = wkt_dim(mls.dim()); + let lines = mls.line_strings().map(|ls| line_string_of(&ls)).collect(); + Wkt::MultiLineString(MultiLineString::new(lines, dim)) + } + GeometryType::MultiPolygon(mp) => { + let dim = wkt_dim(mp.dim()); + let polys = mp.polygons().map(|p| polygon_of(&p)).collect(); + Wkt::MultiPolygon(MultiPolygon::new(polys, dim)) + } + GeometryType::GeometryCollection(gc) => { + // MLT itself never produces a GeometryCollection, but a caller may pass one as + // encoder input. Flatten the contained geometries into a wkt GeometryCollection. + let dim = wkt_dim(gc.dim()); + let geoms = gc.geometries().map(|g| to_wkt(&g)).collect(); + Wkt::GeometryCollection(GeometryCollection::new(geoms, dim)) + } + GeometryType::Rect(r) => Wkt::Polygon(rect_to_polygon(r)), + GeometryType::Triangle(t) => Wkt::Polygon(triangle_to_polygon(t)), + GeometryType::Line(l) => { + let dim = wkt_dim(l.dim()); + let coords = vec![coord_of(&l.start()), coord_of(&l.end())]; + Wkt::LineString(LineString::new(coords, dim)) + } + } +} + +fn rect_to_polygon(r: &impl RectTrait) -> Polygon { + // A rect is inherently 2D; its corners are synthesized from min/max. + let (min, max) = (r.min(), r.max()); + let (x0, y0, x1, y1) = (min.x(), min.y(), max.x(), max.y()); + let xy = |x, y| Coord { + x, + y, + z: None, + m: None, + }; + let coords = vec![xy(x0, y0), xy(x1, y0), xy(x1, y1), xy(x0, y1), xy(x0, y0)]; + Polygon::new(vec![LineString::new(coords, Dimension::XY)], Dimension::XY) +} + +fn triangle_to_polygon(t: &impl TriangleTrait) -> Polygon { + let dim = wkt_dim(t.dim()); + let coords = vec![ + coord_of(&t.first()), + coord_of(&t.second()), + coord_of(&t.third()), + coord_of(&t.first()), + ]; + Polygon::new(vec![LineString::new(coords, dim)], dim) +} diff --git a/rust/mlt-core/src/convert/mod.rs b/rust/mlt-core/src/convert/mod.rs index 90e7a221b..31b5caa21 100644 --- a/rust/mlt-core/src/convert/mod.rs +++ b/rust/mlt-core/src/convert/mod.rs @@ -1,2 +1,3 @@ pub mod geojson; +pub(crate) mod geom; pub mod mvt; diff --git a/rust/mlt-core/src/convert/mvt/decode.rs b/rust/mlt-core/src/convert/mvt/decode.rs index 851bce5b4..9ca9b6cb0 100644 --- a/rust/mlt-core/src/convert/mvt/decode.rs +++ b/rust/mlt-core/src/convert/mvt/decode.rs @@ -35,7 +35,7 @@ pub fn mvt_to_feature_collection(data: impl AsRef<[u8]>) -> MltResult for TileLayer { } tile_features.push(TileFeature { id: feat.id, - geometry: feat.geometry, + geometry: crate::convert::geom::to_wkt(&feat.geometry), properties, }); } diff --git a/rust/mlt-core/src/convert/mvt/encode.rs b/rust/mlt-core/src/convert/mvt/encode.rs index 1843e8c2a..93f2c7939 100644 --- a/rust/mlt-core/src/convert/mvt/encode.rs +++ b/rust/mlt-core/src/convert/mvt/encode.rs @@ -2,6 +2,7 @@ //! delegating wire-format details to the [`fast_mvt`] crate. use fast_mvt::{MvtTileBuilder, MvtValue}; +use geo_traits::to_geo::ToGeoGeometry as _; use crate::decoder::{PropValue, TileLayer}; use crate::{MltError, MltResult}; @@ -16,7 +17,7 @@ pub fn tile_layers_to_mvt(layers: Vec) -> MltResult> { let mut mvt_layer = tile.layer_with_capacity(layer.name, layer.features.len())?; mvt_layer.extent(layer.extent.into()); for feat in layer.features { - let mut feature = mvt_layer.feature(&feat.geometry)?; + let mut feature = mvt_layer.feature(&feat.geometry.to_geometry())?; feature.id(feat.id); for (col_idx, prop) in feat.properties.into_iter().enumerate() { if let Some(name) = layer.property_names.get(col_idx) @@ -99,20 +100,24 @@ mod tests { vec![], vec![TileFeature { id: Some(1), - geometry: Geometry::Polygon(Polygon::new(LineString(ring), vec![])), + geometry: crate::convert::geom::to_wkt(&Geometry::Polygon(Polygon::new( + LineString(ring), + vec![], + ))), properties: vec![], }], ) .unwrap(); let bytes = tile_layers_to_mvt(vec![layer]).unwrap(); let back = mvt_to_tile_layers(bytes).unwrap(); - let Geometry::Polygon(p) = back[0].features()[0].geometry() else { + let wkt::Wkt::Polygon(p) = back[0].features()[0].geometry() else { panic!( "expected polygon, got {:?}", back[0].features()[0].geometry() ); }; - assert_eq!(p.exterior().0.len(), 5); - assert_eq!(p.exterior().0.first(), p.exterior().0.last()); + let exterior = &p.rings()[0]; + assert_eq!(exterior.coords().len(), 5); + assert_eq!(exterior.coords().first(), exterior.coords().last()); } } diff --git a/rust/mlt-core/src/decoder/column.rs b/rust/mlt-core/src/decoder/column.rs index 82d93bcba..70073c945 100644 --- a/rust/mlt-core/src/decoder/column.rs +++ b/rust/mlt-core/src/decoder/column.rs @@ -51,7 +51,12 @@ impl ColumnType { pub(crate) fn has_name(self) -> bool { !matches!( self, - Self::Id | Self::OptId | Self::LongId | Self::OptLongId | Self::Geometry + Self::Id + | Self::OptId + | Self::LongId + | Self::OptLongId + | Self::Geometry + | Self::GeometryZ ) } diff --git a/rust/mlt-core/src/decoder/geometry/decode.rs b/rust/mlt-core/src/decoder/geometry/decode.rs index a4c912dc9..46d8a4020 100644 --- a/rust/mlt-core/src/decoder/geometry/decode.rs +++ b/rust/mlt-core/src/decoder/geometry/decode.rs @@ -2,8 +2,8 @@ use usize_cast::IntoUsize as _; use crate::codecs::varint::parse_varint; use crate::decoder::{ - DictionaryType, GeometryType, GeometryValues, IntEncoding, LengthType, OffsetType, RawGeometry, - RawStream, StreamMeta, StreamType, + CoordDim, DictionaryType, GeometryType, GeometryValues, IntEncoding, LengthType, OffsetType, + RawGeometry, RawStream, StreamMeta, StreamType, }; use crate::errors::AsMltError as _; use crate::utils::SetOptionOnce as _; @@ -219,7 +219,14 @@ pub fn decode_level2_length_stream( impl<'a> RawGeometry<'a> { /// Parse encoded geometry from bytes (expects varint stream count + streams). /// Reserves decoded memory against the parser's budget. - pub fn from_bytes(input: &'a [u8], parser: &mut Parser) -> crate::MltRefResult<'a, Self> { + /// + /// `dim` is the coordinate dimensionality (from the column type); it is not part of the stream + /// data itself and must be supplied by the caller. + pub fn from_bytes( + input: &'a [u8], + dim: CoordDim, + parser: &mut Parser, + ) -> crate::MltRefResult<'a, Self> { let (input, stream_count) = parse_varint::(input)?; let stream_count = stream_count.into_usize(); if stream_count == 0 { @@ -235,6 +242,7 @@ impl<'a> RawGeometry<'a> { &[], ), items: Vec::new(), + dim, }, )); } @@ -243,7 +251,7 @@ impl<'a> RawGeometry<'a> { // Safety: stream_count is validated != 0 let (input, items) = RawStream::parse_multiple(input, stream_count - 1, parser)?; - Ok((input, Self { meta, items })) + Ok((input, Self { meta, items, dim })) } } @@ -252,7 +260,8 @@ impl Decode for RawGeometry<'_> { /// allocation. All streams carry `num_values` in their metadata so every /// charge is pre-hoc. fn decode(self, dec: &mut Decoder) -> MltResult { - let RawGeometry { meta, items } = self; + let RawGeometry { meta, items, dim } = self; + let n_dims = dim.size(); let vector_types = decode_geometry_types(meta, dec)?; let mut geometry_offsets: Option> = None; let mut part_offsets: Option> = None; @@ -266,7 +275,14 @@ impl Decode for RawGeometry<'_> { match stream.meta.stream_type { StreamType::Present => {} StreamType::Data(v) => match v { - DictionaryType::Vertex | DictionaryType::Morton => { + // The plain vertex buffer uses componentwise delta at the column's `n_dims` + // stride. + DictionaryType::Vertex => { + vertices.set_once(stream.decode_i32s_strided(n_dims, dec)?)?; + } + // The Morton dictionary is always 2D (the space-filling curve is 2D-only), so + // it decodes with the default stride regardless of the column dimensionality. + DictionaryType::Morton => { vertices.set_once(stream.decode_i32s(dec)?)?; } _ => Err(MltError::UnexpectedStreamType(stream.meta.stream_type))?, @@ -404,6 +420,7 @@ impl Decode for RawGeometry<'_> { index_buffer, triangles, vertices, + dim, }) } } diff --git a/rust/mlt-core/src/decoder/geometry/geotype.rs b/rust/mlt-core/src/decoder/geometry/geotype.rs index 7b3789302..15eeadf04 100644 --- a/rust/mlt-core/src/decoder/geometry/geotype.rs +++ b/rust/mlt-core/src/decoder/geometry/geotype.rs @@ -1,9 +1,8 @@ use std::ops::Range; -use geo_types::{ - Coord, Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, -}; use usize_cast::IntoUsize as _; +use wkt::Wkt; +use wkt::types::{Coord, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon}; use crate::MltError::{ GeometryIndexOutOfBounds, GeometryOutOfBounds, GeometryVertexOutOfBounds, NoGeometryOffsets, @@ -84,14 +83,19 @@ impl GeometryValues { self.vertices.as_deref() } - /// Build a `GeoJSON` geometry for a single feature at index `i`. + /// Build a [`wkt::Wkt`] geometry for a single feature at index `i`. /// Polygon and `MultiPolygon` rings are closed per `GeoJSON` spec /// (MLT omits the closing vertex). - pub fn to_geojson(&self, index: usize) -> MltResult> { + /// + /// Coordinates are 2D (`Dimension::XY`) or 3D (`Dimension::XYZ`) per `self.dim`. + pub fn to_geojson(&self, index: usize) -> MltResult> { let verts = self.vertices.as_deref().unwrap_or(&[]); let geoms = self.geometry_offsets.as_deref(); let parts = self.part_offsets.as_deref(); let rings = self.ring_offsets.as_deref(); + // Components per vertex (2 = XY, 3 = XYZ). The vertex buffer is interleaved at this stride. + let n = self.dim.size(); + let coord_dim = self.dim.to_wkt(); let off = |s: &[u32], idx: usize, field: &'static str| -> MltResult { s.get(idx) @@ -116,30 +120,34 @@ impl GeometryValues { let vert = |idx: usize| -> MltResult> { verts - .get(idx * 2..idx * 2 + 2) - .map(|s| Coord { x: s[0], y: s[1] }) + .get(idx * n..idx * n + n) + .map(|s| Coord { + x: s[0], + y: s[1], + z: (n >= 3).then(|| s[2]), + m: None, + }) .ok_or(GeometryVertexOutOfBounds { index, vertex: idx, - count: verts.len() / 2, + count: verts.len() / n, }) }; - let line = |r: Range| -> MltResult> { r.map(&vert).collect() }; + let line = |r: Range| -> MltResult> { + let coords: Vec> = r.map(&vert).collect::>()?; + Ok(LineString::new(coords, coord_dim)) + }; let closed_ring = |r: Range| -> MltResult> { let first = r.start; let mut coords: Vec> = r.map(&vert).collect::>()?; coords.push(vert(first)?); - Ok(LineString(coords)) + Ok(LineString::new(coords, coord_dim)) }; let poly_from_rings = |part_rng: Range, r: &[u32]| -> MltResult> { - let mut rings = part_rng + let rings = part_rng .map(|idx| closed_ring(ring_range(r, idx)?)) - .collect::, _>>()? - .into_iter(); - Ok(Polygon::new( - rings.next().unwrap_or_else(|| LineString(vec![])), - rings.collect(), - )) + .collect::, _>>()?; + Ok(Polygon::new(rings, coord_dim)) }; let geom_type = *self @@ -153,7 +161,7 @@ impl GeometryValues { let idx = geoms.map_or(Ok(index), |g| geom_off(g, index))?; let idx = parts.map_or(Ok(idx), |p| part_off(p, idx))?; let idx = rings.map_or(Ok(idx), |r| ring_off(r, idx))?; - Ok(Geometry::::Point(Point(vert(idx)?))) + Ok(Wkt::Point(Point::from_coord(vert(idx)?))) } GeometryType::LineString => { let parts = parts.ok_or(NoPartOffsets(index, geom_type))?; @@ -165,7 +173,7 @@ impl GeometryValues { Some(ring) => ring_range(ring, part_off(parts, part_idx)?)?, None => part_range(parts, part_idx)?, }; - line(vert_range).map(Geometry::::LineString) + line(vert_range).map(Wkt::LineString) } GeometryType::Polygon => { let parts = parts.ok_or(NoPartOffsets(index, geom_type))?; @@ -174,7 +182,7 @@ impl GeometryValues { .map(|geom| geom_off(geom, index)) .transpose()? .unwrap_or(index); - poly_from_rings(part_range(parts, idx)?, rings).map(Geometry::::Polygon) + poly_from_rings(part_range(parts, idx)?, rings).map(Wkt::Polygon) } GeometryType::MultiPoint => { let geoms = geoms.ok_or(NoGeometryOffsets(index, geom_type))?; @@ -192,9 +200,8 @@ impl GeometryValues { (Some(part), None) => geom_rng.map(|idx| vert(part_off(part, idx)?)).collect(), (None, _) => geom_rng.map(&vert).collect(), }; - Ok(Geometry::::MultiPoint(MultiPoint( - coords?.into_iter().map(Point).collect(), - ))) + let points: Vec> = coords?.into_iter().map(Point::from_coord).collect(); + Ok(Wkt::MultiPoint(MultiPoint::new(points, coord_dim))) } GeometryType::MultiLineString => { let geoms = geoms.ok_or(NoGeometryOffsets(index, geom_type))?; @@ -210,7 +217,9 @@ impl GeometryValues { .collect(), None => geom_rng.map(|idx| line(part_range(parts, idx)?)).collect(), }; - Ok(Geometry::::MultiLineString(MultiLineString(lines?))) + Ok(Wkt::MultiLineString(MultiLineString::new( + lines?, coord_dim, + ))) } GeometryType::MultiPolygon => { let geoms = geoms.ok_or(NoGeometryOffsets(index, geom_type))?; @@ -219,7 +228,7 @@ impl GeometryValues { let polys: Vec<_> = geom_range(geoms, index)? .map(|idx| poly_from_rings(part_range(parts, idx)?, rings)) .collect::>()?; - Ok(Geometry::::MultiPolygon(MultiPolygon(polys))) + Ok(Wkt::MultiPolygon(MultiPolygon::new(polys, coord_dim))) } } } diff --git a/rust/mlt-core/src/decoder/geometry/model.rs b/rust/mlt-core/src/decoder/geometry/model.rs index 2110243ec..6250f0e3a 100644 --- a/rust/mlt-core/src/decoder/geometry/model.rs +++ b/rust/mlt-core/src/decoder/geometry/model.rs @@ -12,11 +12,48 @@ use crate::{DecodeState, Lazy}; /// - `Geometry<'a, Parsed>` — decoded [`GeometryValues`] directly (no enum wrapper). pub type Geometry<'a, S = Lazy> = ::LazyOrParsed, GeometryValues>; +/// Coordinate dimensionality of a geometry column: 2D (X, Y) or 3D (X, Y, Z). +/// +/// Corresponds to the [`crate::wire::ColumnType::Geometry`] / `GeometryZ` distinction (and the +/// spec's `ComplexType::GEOMETRY` / `GEOMETRY_Z`). The flat vertex buffer is interleaved at +/// [`Self::size`] components per vertex. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum CoordDim { + /// 2D coordinates `(x, y)`. + #[default] + Xy, + /// 3D coordinates `(x, y, z)`. + Xyz, +} + +impl CoordDim { + /// Components per vertex: 2 for [`Self::Xy`], 3 for [`Self::Xyz`]. + #[must_use] + pub fn size(self) -> usize { + match self { + Self::Xy => 2, + Self::Xyz => 3, + } + } + + /// The matching [`wkt::types::Dimension`] for building output geometries. + #[must_use] + pub fn to_wkt(self) -> wkt::types::Dimension { + match self { + Self::Xy => wkt::types::Dimension::XY, + Self::Xyz => wkt::types::Dimension::XYZ, + } + } +} + /// Raw geometry data as read directly from the tile (borrows from input bytes) #[derive(Debug, PartialEq, Clone)] pub struct RawGeometry<'a> { pub(crate) meta: RawStream<'a>, pub(crate) items: Vec>, + /// Coordinate dimensionality, set at parse time from the column type + /// ([`crate::wire::ColumnType::Geometry`] vs `GeometryZ`). + pub(crate) dim: CoordDim, } /// Parsed (decoded) geometry data @@ -36,6 +73,10 @@ pub struct GeometryValues { pub(crate) triangles: Option>, #[dbg(formatter = "opt_vec_seq")] pub(crate) vertices: Option>, + /// Coordinate dimensionality (defaults to [`CoordDim::Xy`]). The flat `vertices` buffer is + /// interleaved at `dim.size()` components per vertex; offsets count whole vertices regardless + /// of stride. + pub(crate) dim: CoordDim, } /// Types of geometries supported in MLT diff --git a/rust/mlt-core/src/decoder/iterators.rs b/rust/mlt-core/src/decoder/iterators.rs index fbcb50d06..7584dd136 100644 --- a/rust/mlt-core/src/decoder/iterators.rs +++ b/rust/mlt-core/src/decoder/iterators.rs @@ -18,7 +18,6 @@ use std::fmt; -use geo_types::Geometry; use usize_cast::IntoUsize as _; use crate::decoder::{Layer01, ParsedLayer01, ParsedProperty, ParsedScalar, RawProperty}; @@ -229,8 +228,9 @@ impl<'a> ColumnRef<'a> { pub struct FeatureRef<'feat, 'layer: 'feat> { /// Optional feature ID. id: Option, - /// Geometry in [`Geometry`] form (owned, decoded on demand by the iterator). - geometry: Geometry, + /// Decoded geometry as a dimension-aware [`wkt::Wkt`] (owned, decoded on demand by the + /// iterator). Currently always 2D. + geometry: wkt::Wkt, /// Borrowed slice of column descriptors from the layer; used to yield column names. columns: &'layer [ParsedProperty<'layer>], /// Per-feature values in column order, one per slot (scalar, string, or `SharedDict` @@ -245,7 +245,7 @@ impl<'feat, 'layer: 'feat> FeatureRef<'feat, 'layer> { } #[must_use] - pub fn geometry(&self) -> &Geometry { + pub fn geometry(&self) -> &wkt::Wkt { &self.geometry } @@ -530,7 +530,7 @@ impl<'layer> LendingIterator for Layer01FeatureIter<'layer, '_> { #[cfg(test)] mod tests { - use geo_types::Point; + use geo_types::{Geometry, Point}; use serde_json::Value; use super::*; @@ -726,9 +726,10 @@ mod tests { while let Some(r) = iter.next() { geoms.push(r.unwrap().geometry); } - assert_eq!(geoms[0], Geometry::::Point(Point::new(1, 2))); - assert_eq!(geoms[1], Geometry::::Point(Point::new(3, 4))); - assert_eq!(geoms[2], Geometry::::Point(Point::new(5, 6))); + let expect = |x, y| crate::convert::geom::to_wkt(&Geometry::::Point(Point::new(x, y))); + assert_eq!(geoms[0], expect(1, 2)); + assert_eq!(geoms[1], expect(3, 4)); + assert_eq!(geoms[2], expect(5, 6)); } #[test] diff --git a/rust/mlt-core/src/decoder/mod.rs b/rust/mlt-core/src/decoder/mod.rs index 79ff94369..d7e2c5ff9 100644 --- a/rust/mlt-core/src/decoder/mod.rs +++ b/rust/mlt-core/src/decoder/mod.rs @@ -17,8 +17,8 @@ mod tile; // ── Crate-internal re-exports ───────────────────────────────────────────────── // Allow internal modules to keep using `crate::decoder::*` paths without // reaching into sub-module paths explicitly. +pub use geometry::{CoordDim, GeometryType, GeometryValues}; pub(crate) use geometry::{Geometry, RawGeometry}; -pub use geometry::{GeometryType, GeometryValues}; pub use id::ParsedId; // pub (not pub(crate)) so __private module can re-export it pub(crate) use id::{Id, RawId, RawIdValue}; diff --git a/rust/mlt-core/src/decoder/model.rs b/rust/mlt-core/src/decoder/model.rs index 325396876..9373626c9 100644 --- a/rust/mlt-core/src/decoder/model.rs +++ b/rust/mlt-core/src/decoder/model.rs @@ -100,6 +100,10 @@ pub enum ColumnType { LongId = 2, OptLongId = 3, Geometry = 4, + /// 3D geometry column (interleaved `x, y, z` vertices). The in-tile analog of the spec's + /// `ComplexType::GEOMETRY_Z`. Value `6` is even so the odd-LSB "optional" convention + /// (see [`Self::is_optional`]) correctly reports it as non-optional. + GeometryZ = 6, Bool = 10, OptBool = 11, I8 = 12, @@ -217,8 +221,8 @@ pub struct TileLayer { #[derive(Debug, Clone, PartialEq)] pub struct TileFeature { pub(crate) id: Option, - /// Geometry as a [`geo_types`] form - pub(crate) geometry: geo_types::Geometry, + /// Geometry as a dimension-aware [`wkt::Wkt`] (currently always 2D). + pub(crate) geometry: wkt::Wkt, /// One value per property column, in the same order as /// [`TileLayer::property_names`]. pub(crate) properties: Vec, @@ -354,20 +358,23 @@ impl TileLayer { } impl TileFeature { + /// Create a feature from any geometry that implements [`geo_traits::GeometryTrait`] + /// (e.g. a [`geo_types::Geometry`] or a [`wkt::Wkt`]). The geometry is normalized to + /// [`wkt::Wkt`] internally. #[must_use] - pub fn new(geometry: geo_types::Geometry) -> Self { + pub fn new(geometry: &impl geo_traits::GeometryTrait) -> Self { Self { id: None, - geometry, + geometry: crate::convert::geom::to_wkt(geometry), properties: Vec::new(), } } #[must_use] - pub fn with_id(geometry: geo_types::Geometry, id: u64) -> Self { + pub fn with_id(geometry: &impl geo_traits::GeometryTrait, id: u64) -> Self { Self { id: Some(id), - geometry, + geometry: crate::convert::geom::to_wkt(geometry), properties: Vec::new(), } } @@ -378,7 +385,7 @@ impl TileFeature { } #[must_use] - pub fn geometry(&self) -> &geo_types::Geometry { + pub fn geometry(&self) -> &wkt::Wkt { &self.geometry } @@ -426,7 +433,10 @@ impl TileLayerBuilder { self.layer.add_property(name, kind) } - pub fn feature(&mut self, geometry: geo_types::Geometry) -> TileFeatureBuilder<'_> { + pub fn feature( + &mut self, + geometry: &impl geo_traits::GeometryTrait, + ) -> TileFeatureBuilder<'_> { let properties = self .layer .property_kinds @@ -438,7 +448,7 @@ impl TileLayerBuilder { layer: self, feature: TileFeature { id: None, - geometry, + geometry: crate::convert::geom::to_wkt(geometry), properties, }, } @@ -623,7 +633,7 @@ mod tests { fn point_feature(properties: Vec) -> TileFeature { TileFeature { id: None, - geometry: Geometry::Point(Point::new(0, 0)), + geometry: crate::convert::geom::to_wkt(&Geometry::Point(Point::new(0, 0))), properties, } } @@ -716,7 +726,7 @@ mod tests { fn builder_uses_declared_property_kind_for_defaults() { let mut builder = TileLayer::builder("layer", 4096).unwrap(); let flag = builder.add_property("flag", PropKind::Bool).unwrap(); - let mut feature = builder.feature(Geometry::Point(Point::new(0, 0))); + let mut feature = builder.feature(&Geometry::Point(Point::new(0, 0))); feature.property(flag, PropValue::Bool(Some(true))).unwrap(); feature.finish().unwrap(); let layer = builder.finish(); diff --git a/rust/mlt-core/src/decoder/root.rs b/rust/mlt-core/src/decoder/root.rs index 9eb3cc7d0..dfa0f2db6 100644 --- a/rust/mlt-core/src/decoder/root.rs +++ b/rust/mlt-core/src/decoder/root.rs @@ -8,8 +8,8 @@ use crate::MltError::{ }; use crate::codecs::varint::parse_varint; use crate::decoder::{ - Column, ColumnType, DictionaryType, Extent, Geometry, Id, Layer01, ParsedLayer01, RawFsstData, - RawGeometry, RawId, RawIdValue, RawPlainData, RawPresence, RawProperty, RawScalar, + Column, ColumnType, CoordDim, DictionaryType, Extent, Geometry, Id, Layer01, ParsedLayer01, + RawFsstData, RawGeometry, RawId, RawIdValue, RawPlainData, RawPresence, RawProperty, RawScalar, RawSharedDict, RawSharedDictEncoding, RawSharedDictItem, RawStream, RawStrings, RawStringsEncoding, StreamType, }; @@ -316,7 +316,10 @@ impl<'a> Layer01<'a, Lazy> { }))?; } ColumnType::Geometry => { - input = parse_geometry_column(input, &mut geometry, parser)?; + input = parse_geometry_column(input, &mut geometry, CoordDim::Xy, parser)?; + } + ColumnType::GeometryZ => { + input = parse_geometry_column(input, &mut geometry, CoordDim::Xyz, parser)?; } ColumnType::Bool | ColumnType::OptBool => { (input, opt) = parse_optional(column.typ, input, parser)?; @@ -453,6 +456,7 @@ fn parse_optional<'a>( fn parse_geometry_column<'a>( input: &'a [u8], geometry: &mut Option>, + dim: CoordDim, parser: &mut Parser, ) -> MltResult<&'a [u8]> { let (input, stream_count) = parse_varint::(input)?; @@ -468,7 +472,7 @@ fn parse_geometry_column<'a>( let (input, meta) = RawStream::from_bytes(input, parser)?; // geometry items let (input, items) = RawStream::parse_multiple(input, stream_count_capa - 1, parser)?; - geometry.set_once(Raw(RawGeometry { meta, items }))?; + geometry.set_once(Raw(RawGeometry { meta, items, dim }))?; Ok(input) } diff --git a/rust/mlt-core/src/decoder/stream/decode.rs b/rust/mlt-core/src/decoder/stream/decode.rs index 946c5ea94..2fb92d0c7 100644 --- a/rust/mlt-core/src/decoder/stream/decode.rs +++ b/rust/mlt-core/src/decoder/stream/decode.rs @@ -78,6 +78,18 @@ impl<'a> RawStream<'a> { result } + /// Like [`Self::decode_i32s`], but decodes a componentwise-delta vertex stream with the given + /// `n_dims` stride (used for the geometry `VertexBuffer`: 2 for `Geometry`, 3 for `GeometryZ`). + pub fn decode_i32s_strided(self, n_dims: usize, dec: &mut Decoder) -> MltResult> { + let meta = self.meta; + let mut buf = mem::take(&mut dec.buffer_u32); + self.decode_bits_u32(&mut buf, dec)?; + let result = LogicalValue::new(meta).decode_i32_strided(&buf, n_dims, dec); + dec.buffer_u32 = buf; + dec.buffer_u32.clear(); + result + } + pub fn decode_u32s(self, dec: &mut Decoder) -> MltResult> { let meta = self.meta; if meta.encoding.logical == LogicalEncoding::None { diff --git a/rust/mlt-core/src/decoder/stream/logical.rs b/rust/mlt-core/src/decoder/stream/logical.rs index a5bad350c..56876df98 100644 --- a/rust/mlt-core/src/decoder/stream/logical.rs +++ b/rust/mlt-core/src/decoder/stream/logical.rs @@ -5,7 +5,7 @@ use num_traits::{PrimInt, ToPrimitive as _}; use usize_cast::IntoUsize as _; use crate::MltError::{ParsingLogicalTechnique, RleRunLenInvalid, UnsupportedLogicalEncoding}; -use crate::codecs::zigzag::{decode_componentwise_delta_vec2s, decode_zigzag, decode_zigzag_delta}; +use crate::codecs::zigzag::{decode_componentwise_delta, decode_zigzag, decode_zigzag_delta}; use crate::decoder::{LogicalEncoding, LogicalTechnique, LogicalValue, RleMeta, StreamMeta}; use crate::errors::{AsMltError as _, fail_if_invalid_stream_size}; use crate::{Decoder, MltResult}; @@ -64,7 +64,7 @@ impl LogicalValue { match self.meta.encoding.logical { LogicalEncoding::None => decode_zigzag(data, dec), LogicalEncoding::Rle(v) => decode_zigzag(&v.decode(data, dec)?, dec), - LogicalEncoding::ComponentwiseDelta => decode_componentwise_delta_vec2s(data, dec), + LogicalEncoding::ComponentwiseDelta => decode_componentwise_delta(data, 2, dec), LogicalEncoding::Delta => decode_zigzag_delta::(data, dec), LogicalEncoding::DeltaRle(v) => { let expanded = v.decode(data, dec)?; @@ -83,6 +83,22 @@ impl LogicalValue { } } + /// Like [`Self::decode_i32`], but decodes a componentwise-delta vertex stream with the given + /// `n_dims` stride (2 for `Geometry`, 3 for `GeometryZ`). All other logical encodings are + /// stride-independent and delegate to [`Self::decode_i32`]. + pub fn decode_i32_strided( + self, + data: &[u32], + n_dims: usize, + dec: &mut Decoder, + ) -> MltResult> { + if self.meta.encoding.logical == LogicalEncoding::ComponentwiseDelta { + decode_componentwise_delta(data, n_dims, dec) + } else { + self.decode_i32(data, dec) + } + } + /// Logically decode `data` (physically decoded u32 words) into `Vec`. /// /// Not called for `LogicalEncoding::None` — that case is handled entirely diff --git a/rust/mlt-core/src/encoder/geometry/encode.rs b/rust/mlt-core/src/encoder/geometry/encode.rs index d30957ba6..e6ffe5d8c 100644 --- a/rust/mlt-core/src/encoder/geometry/encode.rs +++ b/rust/mlt-core/src/encoder/geometry/encode.rs @@ -9,11 +9,11 @@ use usize_cast::{FromUsize as _, IntoUsize as _}; use super::model::VertexBufferType; use crate::MltResult; use crate::codecs::hilbert::hilbert_sort_key; -use crate::codecs::zigzag::encode_componentwise_delta_vec2s; +use crate::codecs::zigzag::encode_componentwise_delta; use crate::decoder::GeometryType::{LineString, Point, Polygon}; use crate::decoder::{ - ColumnType, DictionaryType, GeometryType, GeometryValues, LengthType, LogicalEncoding, Morton, - OffsetType, PhysicalEncoding, StreamMeta, StreamType, + ColumnType, CoordDim, DictionaryType, GeometryType, GeometryValues, LengthType, + LogicalEncoding, Morton, OffsetType, PhysicalEncoding, StreamMeta, StreamType, }; use crate::encoder::model::{CurveParams, StreamCtx}; use crate::encoder::{Codecs, Encoder, PhysicalCodecs, write_stream_payload}; @@ -384,14 +384,15 @@ fn get_hilbert_params(enc: &Encoder) -> CurveParams { .expect("hilbert_cache populated by StagedLayer::encode_into") } -/// Encode the plain Vec2 vertex layout: componentwise-delta over the raw -/// `[x0, y0, x1, y1, …]` slice. -fn encode_vec2_vertex_stream( +/// Encode the plain vertex layout: componentwise-delta over the raw interleaved +/// `[x0, y0, (z0,) x1, y1, (z1,) …]` slice with the given `n_dims` stride. +fn encode_vecn_vertex_stream( vertices: &[i32], + n_dims: usize, enc: &mut Encoder, codecs: &mut Codecs, ) -> MltResult { - let delta = encode_componentwise_delta_vec2s(vertices, &mut codecs.logical.u32_tmp); + let delta = encode_componentwise_delta(vertices, n_dims, &mut codecs.logical.u32_tmp); let ctx = StreamCtx::geom(StreamType::Data(DictionaryType::Vertex), "vertex"); let logical = LogicalEncoding::ComponentwiseDelta; write_geo_precomputed_stream(delta, ctx, logical, enc, &mut codecs.physical) @@ -454,7 +455,7 @@ fn encode_hilbert_vertex_streams( // Reuse `offsets` as the delta output rather than allocating another Vec; // also keeps `codecs.logical.u32_values` free for the inner race. - encode_componentwise_delta_vec2s(&dict_xy, &mut offsets); + encode_componentwise_delta(&dict_xy, 2, &mut offsets); let ctx = StreamCtx::geom(StreamType::Data(DictionaryType::Vertex), "vertex"); let logical = LogicalEncoding::ComponentwiseDelta; n += write_geo_precomputed_stream(&offsets, ctx, logical, enc, &mut codecs.physical)?; @@ -537,7 +538,9 @@ impl GeometryValues { index_buffer, triangles, vertices, + dim, } = self; + let n_dims = dim.size(); // Flatten every Option → Vec (empty == not present). // triangles: None means no tessellation; Some([]) can't occur in practice (each @@ -576,7 +579,11 @@ impl GeometryValues { // Write column type to meta; reserve exactly 1 byte for stream count // (geometry never exceeds ~8 streams, always fits in a single varint byte). - enc.write_column_type(ColumnType::Geometry)?; + let column_type = match dim { + CoordDim::Xy => ColumnType::Geometry, + CoordDim::Xyz => ColumnType::GeometryZ, + }; + enc.write_column_type(column_type)?; let stream_count_pos = enc.data().len(); enc.data_mut().push(0); // placeholder — patched below let mut n: u8 = 0; @@ -673,9 +680,13 @@ impl GeometryValues { let ctx = StreamCtx::geom(StreamType::Offset(OffsetType::Index), "triangles_indexes"); n += write_geo_u32_stream(&index_buffer, ctx, enc, codecs)?; - if let Some(forced) = enc.override_vertex_buffer_type() { + if dim != CoordDim::Xy { + // Morton/Hilbert space-filling curves are 2D-only, so 3D geometry always uses the + // plain interleaved layout with componentwise delta. + n += encode_vecn_vertex_stream(&vertices, n_dims, enc, codecs)?; + } else if let Some(forced) = enc.override_vertex_buffer_type() { n += match forced { - VertexBufferType::Vec2 => encode_vec2_vertex_stream(&vertices, enc, codecs)?, + VertexBufferType::Vec2 => encode_vecn_vertex_stream(&vertices, 2, enc, codecs)?, VertexBufferType::Morton => encode_morton_vertex_streams(&vertices, enc, codecs)?, VertexBufferType::Hilbert => encode_hilbert_vertex_streams(&vertices, enc, codecs)?, }; @@ -687,7 +698,7 @@ impl GeometryValues { alt.with(|e| { let ds = e.data().len(); let ms = e.meta().len(); - winner_stream_cnt = encode_vec2_vertex_stream(&vertices, e, codecs)?; + winner_stream_cnt = encode_vecn_vertex_stream(&vertices, 2, e, codecs)?; winner_size = (e.data().len() - ds) + (e.meta().len() - ms); Ok(()) })?; @@ -715,7 +726,7 @@ impl GeometryValues { drop(alt); n += winner_stream_cnt; } else { - n += encode_vec2_vertex_stream(&vertices, enc, codecs)?; + n += encode_vecn_vertex_stream(&vertices, 2, enc, codecs)?; } // Patch the reserved stream-count byte. diff --git a/rust/mlt-core/src/encoder/geometry/geotype.rs b/rust/mlt-core/src/encoder/geometry/geotype.rs index 19dcfa79e..622239f27 100644 --- a/rust/mlt-core/src/encoder/geometry/geotype.rs +++ b/rust/mlt-core/src/encoder/geometry/geotype.rs @@ -1,26 +1,39 @@ use geo::{Convert as _, TriangulateEarcut as _}; -use geo_types::{Coord, Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon, Polygon}; - -use crate::decoder::{GeometryType, GeometryValues}; +use geo_traits::to_geo::{ToGeoCoord as _, ToGeoPolygon as _, ToGeoRect as _, ToGeoTriangle as _}; +use geo_traits::{ + CoordTrait, Dimensions, GeometryCollectionTrait as _, GeometryTrait, GeometryType as GeoType, + LineStringTrait, LineTrait as _, MultiLineStringTrait, MultiPointTrait, MultiPolygonTrait, + PointTrait, PolygonTrait, +}; +use geo_types::{LineString, Polygon}; + +use crate::decoder::{CoordDim, GeometryType, GeometryValues}; + +/// Map any [`geo_traits::GeometryTrait`] geometry to its MLT [`GeometryType`]. +/// `Line`, `Rect`, `Triangle` and `GeometryCollection` have no direct MLT geometry type. +pub(crate) fn geometry_type_of(geom: &impl GeometryTrait) -> Result { + Ok(match geom.as_type() { + GeoType::Point(_) => GeometryType::Point, + GeoType::MultiPoint(_) => GeometryType::MultiPoint, + GeoType::LineString(_) => GeometryType::LineString, + GeoType::MultiLineString(_) => GeometryType::MultiLineString, + GeoType::Polygon(_) => GeometryType::Polygon, + GeoType::MultiPolygon(_) => GeometryType::MultiPolygon, + GeoType::Line(_) + | GeoType::GeometryCollection(_) + | GeoType::Rect(_) + | GeoType::Triangle(_) => { + return Err(()); + } + }) +} -impl TryFrom<&Geometry> for GeometryType { +/// Determine the MLT [`GeometryType`] for a decoded [`wkt::Wkt`] geometry. +impl TryFrom<&wkt::Wkt> for GeometryType { type Error = (); - fn try_from(geom: &Geometry) -> Result { - Ok(match geom { - Geometry::::Point(_) => Self::Point, - Geometry::::MultiPoint(_) => Self::MultiPoint, - Geometry::::LineString(_) => Self::LineString, - Geometry::::MultiLineString(_) => Self::MultiLineString, - Geometry::::Polygon(_) => Self::Polygon, - Geometry::::MultiPolygon(_) => Self::MultiPolygon, - Geometry::::Line(_) - | Geometry::::GeometryCollection(_) - | Geometry::::Rect(_) - | Geometry::::Triangle(_) => { - return Err(()); - } - }) + fn try_from(geom: &wkt::Wkt) -> Result { + geometry_type_of(geom) } } @@ -58,11 +71,13 @@ impl GeometryValues { } /// Tessellate `polygon` using the Earcut algorithm and append the results directly into - /// `self.index_buffer` and `self.triangles`. - fn tessellate_polygon(&mut self, polygon: &Polygon) { + /// `self.index_buffer` and `self.triangles`. The polygon is converted to a 2D + /// [`geo_types::Polygon`] for tessellation (any Z is dropped — tessellation is 2D). + fn tessellate_polygon(&mut self, polygon: &impl PolygonTrait) { if let Some(triangles) = self.triangles.as_mut() { + let gt: Polygon = polygon.to_polygon(); let (num_triangles, _) = - earcut_into(polygon, 0, self.index_buffer.get_or_insert_with(Vec::new)); + earcut_into(>, 0, self.index_buffer.get_or_insert_with(Vec::new)); triangles.push(num_triangles); } } @@ -74,13 +89,14 @@ impl GeometryValues { /// preceding polygons so they reference the correct positions in the shared vertex buffer. /// A single total triangle count (summed over all constituent polygons) is pushed into /// `self.triangles`. - fn tessellate_multi_polygon(&mut self, mp: &MultiPolygon) { + fn tessellate_multi_polygon(&mut self, mp: &impl MultiPolygonTrait) { if let Some(triangles) = self.triangles.as_mut() { let mut total_triangles = 0u32; let mut vertex_offset = 0u32; let index_buffer = self.index_buffer.get_or_insert_with(Vec::new); - for poly in &mp.0 { - let (num_triangles, num_verts) = earcut_into(poly, vertex_offset, index_buffer); + for poly in mp.polygons() { + let gt: Polygon = poly.to_polygon(); + let (num_triangles, num_verts) = earcut_into(>, vertex_offset, index_buffer); total_triangles += num_triangles; vertex_offset += num_verts; } @@ -89,44 +105,62 @@ impl GeometryValues { } /// Add a geometry to this decoded geometry collection. - /// This is the reverse of `to_geojson` - it converts a `&Geometry` - /// into the internal MLT representation with offset arrays. + /// This is the reverse of `to_geojson` - it converts any [`geo_traits::GeometryTrait`] + /// geometry into the internal MLT representation with offset arrays. + /// + /// X and Y are always read; Z is read when the column is 3D (see [`Self::push_geom`]). #[must_use] - pub fn with_geom(mut self, geom: &Geometry) -> Self { + pub fn with_geom(mut self, geom: &impl GeometryTrait) -> Self { self.push_geom(geom); self } /// Add a geometry to this decoded geometry collection (mutable version). - pub fn push_geom(&mut self, geom: &Geometry) { - match geom { - Geometry::::Point(p) => self.push_point(p.0), - Geometry::::Line(l) => self.push_linestring(&LineString(vec![l.start, l.end])), - Geometry::::LineString(ls) => self.push_linestring(ls), - Geometry::::Polygon(p) => self.push_polygon(p), - Geometry::::MultiPoint(mp) => self.push_multi_point(mp), - Geometry::::MultiLineString(mls) => self.push_multi_linestring(mls), - Geometry::::MultiPolygon(mp) => self.push_multi_polygon(mp), - Geometry::::Triangle(t) => self.push_polygon(&t.to_polygon()), - Geometry::::Rect(r) => self.push_polygon(&r.to_polygon()), - Geometry::::GeometryCollection(gc) => { - for g in gc { - self.push_geom(g); + /// + /// The column's dimensionality is taken from the first geometry pushed: a geometry whose + /// `dim()` is `Xyz` makes this a 3D (`GeometryZ`) column and subsequent vertices store Z. + /// Mixing 2D and 3D geometries in one column is unsupported. + pub fn push_geom(&mut self, geom: &impl GeometryTrait) { + if self.vector_types.is_empty() { + self.dim = if matches!(geom.dim(), Dimensions::Xyz) { + CoordDim::Xyz + } else { + CoordDim::Xy + }; + } + match geom.as_type() { + GeoType::Point(p) => self.push_point(p), + GeoType::Line(l) => { + let ls = LineString::new(vec![l.start().to_coord(), l.end().to_coord()]); + self.push_linestring(&ls); + } + GeoType::LineString(ls) => self.push_linestring(ls), + GeoType::Polygon(p) => self.push_polygon(p), + GeoType::MultiPoint(mp) => self.push_multi_point(mp), + GeoType::MultiLineString(mls) => self.push_multi_linestring(mls), + GeoType::MultiPolygon(mp) => self.push_multi_polygon(mp), + GeoType::Triangle(t) => self.push_polygon(&t.to_triangle().to_polygon()), + GeoType::Rect(r) => self.push_polygon(&r.to_rect().to_polygon()), + GeoType::GeometryCollection(gc) => { + for g in gc.geometries() { + self.push_geom(&g); } } } } - fn push_point(&mut self, coord: Coord) { + fn push_point(&mut self, point: &impl PointTrait) { self.vector_types.push(GeometryType::Point); - self.vertices - .get_or_insert_with(Vec::new) - .extend([coord.x, coord.y]); + let n_dims = self.dim.size(); + if let Some(c) = point.coord() { + push_vertex(self.vertices.get_or_insert_with(Vec::new), &c, n_dims); + } } - fn push_linestring(&mut self, ls: &LineString) { + fn push_linestring(&mut self, ls: &impl LineStringTrait) { self.vector_types.push(GeometryType::LineString); + let n_dims = self.dim.size(); let verts = self.vertices.get_or_insert_with(Vec::new); // If ring_offsets exists (i.e., there's a Polygon in the layer), // add LineString vertex count to ring_offsets instead of part_offsets. @@ -136,10 +170,11 @@ impl GeometryValues { .as_mut() .unwrap_or_else(|| self.part_offsets.get_or_insert_with(Vec::new)); - push_linestrings(std::iter::once(ls), verts, offsets); + init_offsets(offsets); + push_linestring_coords(ls, verts, offsets, n_dims); } - fn push_polygon(&mut self, poly: &Polygon) { + fn push_polygon(&mut self, poly: &impl PolygonTrait) { // Only on the very first polygon: if LineStrings were pushed before us, // their vertex offsets are sitting in part_offsets. Move them to // ring_offsets now, before we set up ring_offsets for polygon use. @@ -148,11 +183,12 @@ impl GeometryValues { self.vector_types.push(GeometryType::Polygon); self.init_polygon_offsets(); + let n_dims = self.dim.size(); let verts = self.vertices.get_or_insert_with(Vec::new); let rings = self.ring_offsets.as_mut().unwrap(); let parts = self.part_offsets.as_mut().unwrap(); - push_polygon_rings(poly, verts, rings, parts); + push_polygon_rings(poly, verts, rings, parts, n_dims); self.tessellate_polygon(poly); } @@ -168,20 +204,24 @@ impl GeometryValues { init_offsets(self.part_offsets.get_or_insert_with(Vec::new)); } - fn push_multi_point(&mut self, mp: &MultiPoint) { + fn push_multi_point(&mut self, mp: &impl MultiPointTrait) { self.vector_types.push(GeometryType::MultiPoint); + let n_dims = self.dim.size(); let verts = self.vertices.get_or_insert_with(Vec::new); - for point in mp { - verts.extend([point.0.x, point.0.y]); + for point in mp.points() { + if let Some(c) = point.coord() { + push_vertex(verts, &c, n_dims); + } } - self.push_geometry_count(u32::try_from(mp.0.len()).expect("point count overflow")); + self.push_geometry_count(u32::try_from(mp.num_points()).expect("point count overflow")); } - fn push_multi_linestring(&mut self, mls: &MultiLineString) { + fn push_multi_linestring(&mut self, mls: &impl MultiLineStringTrait) { self.vector_types.push(GeometryType::MultiLineString); + let n_dims = self.dim.size(); let verts = self.vertices.get_or_insert_with(Vec::new); // When a Polygon is present (ring_offsets exists), LineString vertex counts // go to ring_offsets instead of part_offsets. This matches Java's behavior. @@ -190,24 +230,30 @@ impl GeometryValues { .as_mut() .unwrap_or_else(|| self.part_offsets.get_or_insert_with(Vec::new)); - push_linestrings(mls.iter(), verts, offsets); + init_offsets(offsets); + for ls in mls.line_strings() { + push_linestring_coords(&ls, verts, offsets, n_dims); + } - self.push_geometry_count(u32::try_from(mls.0.len()).expect("linestring count overflow")); + self.push_geometry_count( + u32::try_from(mls.num_line_strings()).expect("linestring count overflow"), + ); } - fn push_multi_polygon(&mut self, mp: &MultiPolygon) { + fn push_multi_polygon(&mut self, mp: &impl MultiPolygonTrait) { self.vector_types.push(GeometryType::MultiPolygon); self.init_polygon_offsets(); + let n_dims = self.dim.size(); let verts = self.vertices.get_or_insert_with(Vec::new); let rings = self.ring_offsets.as_mut().unwrap(); let parts = self.part_offsets.as_mut().unwrap(); - for poly in mp { - push_polygon_rings(poly, verts, rings, parts); + for poly in mp.polygons() { + push_polygon_rings(&poly, verts, rings, parts, n_dims); } - self.push_geometry_count(u32::try_from(mp.0.len()).expect("polygon count overflow")); + self.push_geometry_count(u32::try_from(mp.num_polygons()).expect("polygon count overflow")); self.tessellate_multi_polygon(mp); } @@ -226,56 +272,75 @@ fn init_offsets(v: &mut Vec) { } } +/// Append one vertex to the interleaved buffer: `x, y` and, when `n_dims >= 3`, `z` +/// (`coord.nth(2)`, defaulting to 0 when absent). +fn push_vertex(verts: &mut Vec, c: &impl CoordTrait, n_dims: usize) { + verts.push(c.x()); + verts.push(c.y()); + if n_dims >= 3 { + verts.push(c.nth(2).unwrap_or(0)); + } +} + /// Push a single polygon's rings (exterior + interiors) to the offset arrays. /// MLT omits closing vertices, so we strip them if present. fn push_polygon_rings( - poly: &Polygon, + poly: &impl PolygonTrait, verts: &mut Vec, rings: &mut Vec, parts: &mut Vec, + n_dims: usize, ) { let mut ring_count = *parts.last().unwrap(); - for ring in std::iter::once(poly.exterior()).chain(poly.interiors()) { - push_ring(ring, verts, rings); + for ring in poly.exterior().into_iter().chain(poly.interiors()) { + push_ring(&ring, verts, rings, n_dims); ring_count += 1; } parts.push(ring_count); } /// Push a ring's coordinates (stripping closing vertex) to verts and update rings offset. -fn push_ring(ring: &LineString, verts: &mut Vec, rings: &mut Vec) { - let coords = &ring.0; - let len = if coords.len() > 1 && coords.last() == coords.first() { - coords.len() - 1 - } else { - coords.len() - }; - for c in &coords[..len] { - verts.extend([c.x, c.y]); +fn push_ring( + ring: &impl LineStringTrait, + verts: &mut Vec, + rings: &mut Vec, + n_dims: usize, +) { + let n = ring.num_coords(); + // Strip the closing vertex if the ring repeats its first coordinate at the end. + let closed = n > 1 + && ring + .coord(0) + .zip(ring.coord(n - 1)) + .is_some_and(|(f, l)| f.x() == l.x() && f.y() == l.y()); + let len = if closed { n - 1 } else { n }; + for c in ring.coords().take(len) { + push_vertex(verts, &c, n_dims); } let prev = *rings.last().unwrap(); rings.push(prev + u32::try_from(len).expect("vertex count overflow")); } -/// Push linestrings to vertex buffer and offset array. -fn push_linestrings<'a>( - iter: impl Iterator>, +/// Push a single line string's coordinates to the vertex buffer and update the offset array. +/// The caller is responsible for calling [`init_offsets`] beforehand. +fn push_linestring_coords( + ls: &impl LineStringTrait, verts: &mut Vec, offsets: &mut Vec, + n_dims: usize, ) { - init_offsets(offsets); - for ls in iter { - for c in ls.coords() { - verts.extend([c.x, c.y]); - } - let prev = *offsets.last().unwrap(); - offsets.push(prev + u32::try_from(ls.0.len()).expect("vertex count overflow")); + for c in ls.coords() { + push_vertex(verts, &c, n_dims); } + let prev = *offsets.last().unwrap(); + offsets.push(prev + u32::try_from(ls.num_coords()).expect("vertex count overflow")); } #[cfg(test)] mod tests { - use geo_types::{LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, wkt}; + use geo_types::{ + Coord, Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, wkt, + }; use insta::assert_snapshot; use integer_encoding::VarInt; use proptest::prelude::*; @@ -305,7 +370,11 @@ mod tests { .write_to(&mut enc, &mut codecs) .expect("Failed to encode"); - let parsed = assert_empty(RawGeometry::from_bytes(enc.data(), &mut parser())); + let parsed = assert_empty(RawGeometry::from_bytes( + enc.data(), + CoordDim::Xy, + &mut parser(), + )); LazyParsed::Raw(parsed) .into_parsed(&mut dec()) @@ -620,7 +689,7 @@ mod tests { let buffer = enc.data().to_vec(); let mut p = parser(); - let parsed = assert_empty(RawGeometry::from_bytes(&buffer, &mut p)); + let parsed = assert_empty(RawGeometry::from_bytes(&buffer, CoordDim::Xy, &mut p)); assert_snapshot!(p.reserved(), @"72"); let mut d = dec(); @@ -629,7 +698,8 @@ mod tests { assert_eq!(decoded.vertices, Some(vec![0i32, 0, 4, 0, 0, 4, 4, 0])); let geom = decoded.to_geojson(0).unwrap(); - assert_eq!(geom, wkt!(LINESTRING(0 0,4 0,0 4,4 0)).into()); + let expected = Geometry::from(wkt!(LINESTRING(0 0,4 0,0 4,4 0))); + assert_eq!(geom, crate::convert::geom::to_wkt(&expected)); } mod tessellation_tests { diff --git a/rust/mlt-core/src/encoder/geometry/tests.rs b/rust/mlt-core/src/encoder/geometry/tests.rs index d975f4864..2503d5bc7 100644 --- a/rust/mlt-core/src/encoder/geometry/tests.rs +++ b/rust/mlt-core/src/encoder/geometry/tests.rs @@ -8,7 +8,7 @@ use crate::decoder::RawGeometry; use crate::encoder::model::EncoderConfig; use crate::encoder::{Codecs, Encoder, ExplicitEncoder, IntEncoder, VertexBufferType}; use crate::test_helpers::{assert_empty, dec, parser}; -use crate::{Decode as _, DictionaryType, GeometryValues, LengthType, StreamType}; +use crate::{CoordDim, Decode as _, DictionaryType, GeometryValues, LengthType, StreamType}; #[rstest] #[case::single_point(push_geoms(&[wkt!(POINT(10 20)).into()]))] @@ -90,7 +90,11 @@ fn encoded_output_always_has_meta_stream() { decoded .write_to(&mut enc, &mut codecs) .expect("encode failed"); - let raw = assert_empty(RawGeometry::from_bytes(enc.data(), &mut parser())); + let raw = assert_empty(RawGeometry::from_bytes( + enc.data(), + CoordDim::Xy, + &mut parser(), + )); assert_eq!( raw.meta.meta.stream_type, @@ -151,7 +155,7 @@ fn forced_vertex_strategy_streams( /// Multipoint with repeated coordinates so dict paths actually dedup. fn repeated_multipoint() -> GeometryValues { let mut g = GeometryValues::default(); - g.push_geom(&wkt!(MULTIPOINT(5 5, 10 10, 5 5, 10 10, 0 0, 5 5)).into()); + g.push_geom(&wkt!(MULTIPOINT(5 5, 10 10, 5 5, 10 10, 0 0, 5 5))); g } @@ -235,7 +239,7 @@ fn manual_encode_works() { fn assert_geometry_roundtrip(data: &[u8], expected: &GeometryValues) { let mut p = parser(); let mut d = dec(); - let raw = assert_empty(RawGeometry::from_bytes(data, &mut p)); + let raw = assert_empty(RawGeometry::from_bytes(data, CoordDim::Xy, &mut p)); let result = raw.decode(&mut d).unwrap(); assert!( d.consumed() > 0, @@ -254,8 +258,115 @@ fn push_geoms(geoms: &[Geometry]) -> GeometryValues { /// Collect all stream types present in the encoded geometry bytes (meta + items). fn encoded_stream_types(data: &[u8]) -> HashSet { - let raw = assert_empty(RawGeometry::from_bytes(data, &mut parser())); + let raw = assert_empty(RawGeometry::from_bytes(data, CoordDim::Xy, &mut parser())); std::iter::once(raw.meta.meta.stream_type) .chain(raw.items.iter().map(|s| s.meta.stream_type)) .collect() } + +/// End-to-end 3D (`GeometryZ`) coverage: encode a layer with 3D geometries to wire bytes, reparse, +/// and decode, verifying the `GeometryZ` column marker and the interleaved Z coordinate survive. +mod three_d { + use wkt::Wkt; + use wkt::types::{Coord, Dimension, LineString, MultiPoint, Point, Polygon}; + + use crate::encoder::stage_tile; + use crate::encoder::{ + Codecs, Encoder, EncoderConfig, ExplicitEncoder, IntEncoder, SortStrategy, + }; + use crate::test_helpers::{assert_empty, dec, into_layer01, parser}; + use crate::{Layer, TileFeature, TileLayer}; + + fn xyz(x: i32, y: i32, z: i32) -> Coord { + Coord { + x, + y, + z: Some(z), + m: None, + } + } + + /// Round-trip a single geometry through the full tile encode → wire → decode path and return + /// the decoded geometry as produced by the decoder. + fn tile_roundtrip(geom: &Wkt) -> Wkt { + let tile = + TileLayer::from_parts("t", 4096, vec![], vec![TileFeature::with_id(geom, 1)]).unwrap(); + + let cfg = EncoderConfig::default(); + let enc = Encoder::with_explicit(cfg, ExplicitEncoder::for_id(IntEncoder::varint())); + let mut codecs = Codecs::default(); + let enc = stage_tile(tile, SortStrategy::Unsorted, false, false) + .encode_into(enc, &mut codecs) + .expect("encode failed"); + let buf = enc.into_layer_bytes().expect("into_layer_bytes failed"); + + let mut p = parser(); + let layer_back = assert_empty(Layer::from_bytes(&buf, &mut p)); + let mut d = dec(); + let tile = into_layer01(layer_back) + .into_tile(&mut d) + .expect("decode failed"); + tile.features()[0].geometry().clone() + } + + #[test] + fn point_z_roundtrip() { + let g = Wkt::Point(Point::from_coord(xyz(10, 20, 30))); + let out = tile_roundtrip(&g); + assert_eq!(out, g); + assert_eq!(out.dimension(), Dimension::XYZ); + } + + #[test] + fn line_string_z_roundtrip() { + let g = Wkt::LineString(LineString::new( + vec![xyz(1, 2, 3), xyz(4, 5, 6), xyz(7, 8, 9)], + Dimension::XYZ, + )); + assert_eq!(tile_roundtrip(&g), g); + } + + #[test] + fn multi_point_z_roundtrip() { + let g = Wkt::MultiPoint(MultiPoint::new( + vec![ + Point::from_coord(xyz(1, 2, 3)), + Point::from_coord(xyz(4, 5, 6)), + ], + Dimension::XYZ, + )); + assert_eq!(tile_roundtrip(&g), g); + } + + #[test] + fn polygon_z_roundtrip() { + // Closed ring on input; MLT strips the closing vertex and the decoder re-closes it, + // so the round-tripped geometry matches the (closed) input. + let ring = LineString::new( + vec![ + xyz(0, 0, 1), + xyz(10, 0, 2), + xyz(10, 10, 3), + xyz(0, 10, 4), + xyz(0, 0, 1), + ], + Dimension::XYZ, + ); + let g = Wkt::Polygon(Polygon::new(vec![ring], Dimension::XYZ)); + assert_eq!(tile_roundtrip(&g), g); + } + + /// A 2D geometry must still produce a plain `Geometry` column with no Z (regression guard). + #[test] + fn point_2d_stays_2d() { + let g = Wkt::Point(Point::from_coord(Coord { + x: 7, + y: 8, + z: None, + m: None, + })); + let out = tile_roundtrip(&g); + assert_eq!(out, g); + assert_eq!(out.dimension(), Dimension::XY); + } +} diff --git a/rust/mlt-core/src/encoder/property/tests.rs b/rust/mlt-core/src/encoder/property/tests.rs index ce440aba7..369c034f7 100644 --- a/rust/mlt-core/src/encoder/property/tests.rs +++ b/rust/mlt-core/src/encoder/property/tests.rs @@ -694,7 +694,7 @@ fn tile_from_cols(cols: &[(&str, Vec)]) -> TileLayer { fn try_tile_from_cols(cols: &[(&str, Vec)]) -> MltResult { let n = cols.first().map_or(0, |(_, v)| v.len()); let property_names = cols.iter().map(|(name, _)| (*name).to_string()).collect(); - let geom = geo_types::Geometry::::Point(Point::new(0, 0)); + let geom = crate::convert::geom::to_wkt(&geo_types::Geometry::::Point(Point::new(0, 0))); let features = (0..n) .map(|i| TileFeature { id: None, @@ -717,7 +717,7 @@ fn tile_from_cols_with_ids(ids: &[Option], cols: &[(&str, Vec)]) } fn tile_from_ids(ids: &[Option]) -> TileLayer { - let geom = geo_types::Geometry::::Point(Point::new(0, 0)); + let geom = crate::convert::geom::to_wkt(&geo_types::Geometry::::Point(Point::new(0, 0))); TileLayer::from_parts( "test", 4096, diff --git a/rust/mlt-core/src/encoder/sort.rs b/rust/mlt-core/src/encoder/sort.rs index aa18d4d28..abb797160 100644 --- a/rust/mlt-core/src/encoder/sort.rs +++ b/rust/mlt-core/src/encoder/sort.rs @@ -1,7 +1,11 @@ //! Feature reordering for the optimizer -use geo::CoordsIter as _; -use geo_types::{Coord, Geometry}; +use geo_traits::{ + CoordTrait, GeometryCollectionTrait as _, GeometryTrait, GeometryType as GeoType, + LineStringTrait as _, LineTrait as _, MultiLineStringTrait as _, MultiPointTrait as _, + MultiPolygonTrait as _, PointTrait as _, PolygonTrait, RectTrait as _, TriangleTrait as _, +}; +use geo_types::Coord; use crate::codecs::hilbert::{hilbert_curve_params_from_bounds, hilbert_sort_key}; use crate::codecs::morton::morton_sort_key; @@ -79,32 +83,114 @@ impl TileLayer { #[hotpath::measure] #[must_use] pub fn curve_params(&self) -> CurveParams { - let (min_val, max_val) = self - .features() - .iter() - .flat_map(|f| f.geometry().coords_iter()) - .fold((i32::MAX, i32::MIN), |(min, max), c| { - (min.min(c.x).min(c.y), max.max(c.x).max(c.y)) + let mut min_val = i32::MAX; + let mut max_val = i32::MIN; + for f in self.features() { + for_each_coord(f.geometry(), &mut |x, y| { + min_val = min_val.min(x).min(y); + max_val = max_val.max(x).max(y); }); + } hilbert_curve_params_from_bounds(min_val, max_val) } } -/// Extract the coordinate of the first vertex of a geometry. -fn first_vertex(geom: &Geometry) -> Option> { - match geom { - Geometry::::Point(p) => Some(p.0), - Geometry::::Line(l) => Some(l.start), - Geometry::::LineString(ls) => ls.0.first().copied(), - Geometry::::Polygon(p) => p.exterior().0.first().copied(), - Geometry::::MultiPoint(mp) => mp.0.first().map(|p| p.0), - Geometry::::MultiLineString(mls) => mls.0.first().and_then(|ls| ls.0.first().copied()), - Geometry::::MultiPolygon(mp) => { - mp.0.first().and_then(|p| p.exterior().0.first().copied()) +/// Extract the coordinate of the first vertex of a geometry (X/Y only). +/// +/// This reads only the first coordinate (no full traversal). +fn first_vertex(geom: &impl GeometryTrait) -> Option> { + // The associated coordinate types borrow from owned intermediates (e.g. a ring returned by + // `exterior()`), so each `Coord` must be materialized inside the closure that owns them. + match geom.as_type() { + GeoType::Point(p) => p.coord().map(|c| coord_xy(&c)), + GeoType::Line(l) => Some(coord_xy(&l.start())), + GeoType::LineString(ls) => ls.coords().next().map(|c| coord_xy(&c)), + GeoType::Polygon(p) => p + .exterior() + .and_then(|r| r.coords().next().map(|c| coord_xy(&c))), + GeoType::MultiPoint(mp) => mp + .points() + .next() + .and_then(|p| p.coord().map(|c| coord_xy(&c))), + GeoType::MultiLineString(mls) => mls + .line_strings() + .next() + .and_then(|ls| ls.coords().next().map(|c| coord_xy(&c))), + GeoType::MultiPolygon(mp) => mp.polygons().next().and_then(|p| { + p.exterior() + .and_then(|r| r.coords().next().map(|c| coord_xy(&c))) + }), + GeoType::Triangle(t) => Some(coord_xy(&t.first())), + GeoType::Rect(r) => Some(coord_xy(&r.min())), + GeoType::GeometryCollection(gc) => gc.geometries().next().and_then(|g| first_vertex(&g)), + } +} + +/// Materialize a 2D [`Coord`] from any [`CoordTrait`] (X/Y only). +fn coord_xy(c: &impl CoordTrait) -> Coord { + Coord { x: c.x(), y: c.y() } +} + +/// Visit every X/Y coordinate of a geometry, calling `f(x, y)` for each. +fn for_each_coord(geom: &impl GeometryTrait, f: &mut impl FnMut(i32, i32)) { + fn poly_coords(poly: &impl PolygonTrait, f: &mut impl FnMut(i32, i32)) { + for ring in poly.exterior().into_iter().chain(poly.interiors()) { + for c in ring.coords() { + f(c.x(), c.y()); + } + } + } + match geom.as_type() { + GeoType::Point(p) => { + if let Some(c) = p.coord() { + f(c.x(), c.y()); + } + } + GeoType::Line(l) => { + let (s, e) = (l.start(), l.end()); + f(s.x(), s.y()); + f(e.x(), e.y()); + } + GeoType::LineString(ls) => { + for c in ls.coords() { + f(c.x(), c.y()); + } + } + GeoType::Polygon(p) => poly_coords(p, f), + GeoType::MultiPoint(mp) => { + for pt in mp.points() { + if let Some(c) = pt.coord() { + f(c.x(), c.y()); + } + } + } + GeoType::MultiLineString(mls) => { + for ls in mls.line_strings() { + for c in ls.coords() { + f(c.x(), c.y()); + } + } + } + GeoType::MultiPolygon(mp) => { + for poly in mp.polygons() { + poly_coords(&poly, f); + } + } + GeoType::Triangle(t) => { + for c in [t.first(), t.second(), t.third()] { + f(c.x(), c.y()); + } + } + GeoType::Rect(r) => { + let (mn, mx) = (r.min(), r.max()); + f(mn.x(), mn.y()); + f(mx.x(), mx.y()); + } + GeoType::GeometryCollection(gc) => { + for g in gc.geometries() { + for_each_coord(&g, f); + } } - Geometry::::Triangle(t) => Some(t.v1()), - Geometry::::Rect(r) => Some(r.min()), - Geometry::::GeometryCollection(gc) => gc.0.first().and_then(first_vertex), } } @@ -150,7 +236,9 @@ pub(crate) fn spatial_sort_likely_to_help(layer: &TileLayer) -> bool { mod tests { use geo_types::{Coord, Geometry as GeoGeom, Geometry, LineString, Point, Polygon}; - use crate::decoder::{GeometryType, GeometryValues, RawGeometry, TileFeature, TileLayer}; + use crate::decoder::{ + CoordDim, GeometryType, GeometryValues, RawGeometry, TileFeature, TileLayer, + }; use crate::encoder::{ Codecs, Encoder, EncoderConfig, ExplicitEncoder, IntEncoder, SortStrategy, stage_tile, }; @@ -197,7 +285,7 @@ mod tests { .expect("encode failed"); let buf = enc.data().to_vec(); - let parsed = assert_empty(RawGeometry::from_bytes(&buf, &mut parser())); + let parsed = assert_empty(RawGeometry::from_bytes(&buf, CoordDim::Xy, &mut parser())); let mut d = dec(); let result = LazyParsed::Raw(parsed) .into_parsed(&mut d) @@ -226,7 +314,7 @@ mod tests { .zip(ids.iter()) .map(|(g, &id)| TileFeature { id: Some(id), - geometry: g.clone(), + geometry: crate::convert::geom::to_wkt(g), properties: vec![], }) .collect(); @@ -376,7 +464,7 @@ mod tests { .zip(ids.iter()) .map(|(g, &id)| TileFeature { id, - geometry: g.clone(), + geometry: crate::convert::geom::to_wkt(g), properties: vec![], }) .collect(), diff --git a/rust/mlt-core/src/lib.rs b/rust/mlt-core/src/lib.rs index 1956baabf..3c091d5f6 100644 --- a/rust/mlt-core/src/lib.rs +++ b/rust/mlt-core/src/lib.rs @@ -14,8 +14,11 @@ macro_rules! validate_stream { }; } -// re-export geo types -pub use geo_types; +// re-export geo crates. +// `geo_types` is still accepted as encoder input (it implements `geo_traits::GeometryTrait`) +// and used at the MVT boundary. `wkt` provides the dimension-aware geometry types used as the +// decoder output (`wkt::Wkt`), which can represent an optional Z coordinate. +pub use {geo_traits, geo_types, wkt}; pub(crate) mod codecs; pub(crate) mod convert; @@ -26,7 +29,7 @@ pub(crate) mod utils; pub use convert::{geojson, mvt}; pub use decoder::{ - ColumnRef, Decoder, Extent, FeatureRef, GeometryType, GeometryValues, Layer, Layer01, + ColumnRef, CoordDim, Decoder, Extent, FeatureRef, GeometryType, GeometryValues, Layer, Layer01, Layer01FeatureIter, LendingIterator, ParsedLayer, ParsedLayer01, Parser, PropKind, PropName, PropValue, PropValueRef, PropertyKey, TileFeature, TileFeatureBuilder, TileLayer, TileLayerBuilder, Unknown, diff --git a/rust/mlt-py/src/encode/geojson.rs b/rust/mlt-py/src/encode/geojson.rs index 0444c318f..4ea16d0d4 100644 --- a/rust/mlt-py/src/encode/geojson.rs +++ b/rust/mlt-py/src/encode/geojson.rs @@ -11,8 +11,8 @@ use std::collections::HashMap; -use mlt_core::geo_types::Geometry; use mlt_core::geojson::FeatureCollection; +use mlt_core::wkt::Wkt; use mlt_core::{PropKind, PropValue, TileLayer}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -80,17 +80,25 @@ pub fn encode_geojson( Ok(PyBytes::new(py, &bytes).unbind()) } -fn validate_non_empty(g: &Geometry) -> PyResult<()> { +fn validate_non_empty(g: &Wkt) -> PyResult<()> { + let ring_non_empty = |p: &mlt_core::wkt::types::Polygon| { + p.rings() + .first() + .is_some_and(|ext| !ext.coords().is_empty()) + }; let non_empty = match g { - Geometry::Point(_) => true, - Geometry::MultiPoint(mp) => !mp.0.is_empty(), - Geometry::LineString(l) => !l.0.is_empty(), - Geometry::MultiLineString(ml) => !ml.0.is_empty() && ml.0.iter().all(|l| !l.0.is_empty()), - Geometry::Polygon(p) => !p.exterior().0.is_empty(), - Geometry::MultiPolygon(mp) => { - !mp.0.is_empty() && mp.0.iter().all(|p| !p.exterior().0.is_empty()) + Wkt::Point(p) => p.coord().is_some(), + Wkt::MultiPoint(mp) => !mp.points().is_empty(), + Wkt::LineString(l) => !l.coords().is_empty(), + Wkt::MultiLineString(ml) => { + !ml.line_strings().is_empty() + && ml.line_strings().iter().all(|l| !l.coords().is_empty()) + } + Wkt::Polygon(p) => ring_non_empty(p), + Wkt::MultiPolygon(mp) => { + !mp.polygons().is_empty() && mp.polygons().iter().all(ring_non_empty) } - _ => false, + Wkt::GeometryCollection(_) => false, }; if non_empty { Ok(()) @@ -220,7 +228,7 @@ fn build_layer(fc: FeatureCollection, name: String, extent: u32) -> PyResult, coord: [i32; 2]) { - buf.extend_from_slice(&f64::from(coord[0]).to_le_bytes()); - buf.extend_from_slice(&f64::from(coord[1]).to_le_bytes()); -} - -fn push_coord_xform(buf: &mut Vec, coord: [i32; 2], xf: TileTransform) { - let [x, y] = xf.apply(coord); +/// Append one coordinate as little-endian f64 `x, y` (and `z` when `has_z`). +/// +/// The tile transform applies to the planar `x`/`y` only; `z` is elevation and is written as-is. +fn push_coord(buf: &mut Vec, c: &Coord, has_z: bool, xf: Option) { + let [x, y] = match xf { + Some(xf) => xf.apply([c.x, c.y]), + None => [f64::from(c.x), f64::from(c.y)], + }; buf.extend_from_slice(&x.to_le_bytes()); buf.extend_from_slice(&y.to_le_bytes()); -} - -fn push_coord(buf: &mut Vec, coord: [i32; 2], xf: Option) { - match xf { - Some(xf) => push_coord_xform(buf, coord, xf), - None => push_coord_raw(buf, coord), + if has_z { + buf.extend_from_slice(&f64::from(c.z.unwrap_or(0)).to_le_bytes()); } } @@ -71,73 +66,85 @@ fn push_u32(buf: &mut Vec, v: u32) { buf.extend_from_slice(&v.to_le_bytes()); } -fn push_rings( - buf: &mut Vec, - rings: impl IntoIterator>>, - xf: Option, -) { - for ring in rings { - push_u32(buf, ring.0.len() as u32); - for c in &ring.0 { - push_coord(buf, (*c).into(), xf); - } +/// ISO WKB geometry type code: `base` for 2D, `base + 1000` for the Z (3D) variant. +fn wkb_type(base: u32, has_z: bool) -> u32 { + if has_z { base + 1000 } else { base } +} + +/// Append a coordinate sequence: a `u32` count followed by each coordinate. +fn push_coords(buf: &mut Vec, coords: &[Coord], has_z: bool, xf: Option) { + push_u32(buf, coords.len() as u32); + for c in coords { + push_coord(buf, c, has_z, xf); } } fn push_linestring( buf: &mut Vec, - line: impl Deref>, + line: &LineString, + has_z: bool, xf: Option, ) { buf.push(0x01); - push_u32(buf, 2); - push_rings(buf, once(line), xf); + push_u32(buf, wkb_type(2, has_z)); + push_coords(buf, line.coords(), has_z, xf); } -fn push_polygon(buf: &mut Vec, poly: &Polygon, xf: Option) { +fn push_polygon(buf: &mut Vec, poly: &Polygon, has_z: bool, xf: Option) { buf.push(0x01); - push_u32(buf, 3); - push_u32(buf, (poly.interiors().len() + 1) as u32); - push_rings(buf, once(poly.exterior()).chain(poly.interiors()), xf); + push_u32(buf, wkb_type(3, has_z)); + push_u32(buf, poly.rings().len() as u32); + for ring in poly.rings() { + push_coords(buf, ring.coords(), has_z, xf); + } } -fn geom32_to_wkb(geom: &Geometry, xf: Option) -> MltResult> { +/// Encode a decoded geometry as WKB. 3D geometries are emitted as ISO WKB with the Z coordinate +/// preserved (type code `base + 1000`); 2D geometries are unchanged. +fn geom32_to_wkb(geom: &Wkt, xf: Option) -> MltResult> { + let has_z = matches!(geom.dimension(), Dimension::XYZ); let mut buf = Vec::with_capacity(128); match geom { - Geometry::::Point(c) => { + Wkt::Point(p) => { buf.push(0x01); - push_u32(&mut buf, 1); - push_coord(&mut buf, (*c).into(), xf); + push_u32(&mut buf, wkb_type(1, has_z)); + if let Some(c) = p.coord() { + push_coord(&mut buf, c, has_z, xf); + } } - Geometry::::LineString(coords) => push_linestring(&mut buf, coords, xf), - Geometry::::Polygon(poly) => push_polygon(&mut buf, poly, xf), - Geometry::::MultiPoint(coords) => { + Wkt::LineString(line) => push_linestring(&mut buf, line, has_z, xf), + Wkt::Polygon(poly) => push_polygon(&mut buf, poly, has_z, xf), + Wkt::MultiPoint(mp) => { buf.push(0x01); - push_u32(&mut buf, 4); - push_u32(&mut buf, coords.0.len() as u32); - for c in &coords.0 { + push_u32(&mut buf, wkb_type(4, has_z)); + push_u32(&mut buf, mp.points().len() as u32); + for p in mp.points() { buf.push(0x01); - push_u32(&mut buf, 1); - push_coord(&mut buf, (*c).into(), xf); + push_u32(&mut buf, wkb_type(1, has_z)); + if let Some(c) = p.coord() { + push_coord(&mut buf, c, has_z, xf); + } } } - Geometry::::MultiLineString(lines) => { + Wkt::MultiLineString(mls) => { buf.push(0x01); - push_u32(&mut buf, 5); - push_u32(&mut buf, lines.0.len() as u32); - for line in &lines.0 { - push_linestring(&mut buf, line, xf); + push_u32(&mut buf, wkb_type(5, has_z)); + push_u32(&mut buf, mls.line_strings().len() as u32); + for line in mls.line_strings() { + push_linestring(&mut buf, line, has_z, xf); } } - Geometry::::MultiPolygon(polygons) => { + Wkt::MultiPolygon(mp) => { buf.push(0x01); - push_u32(&mut buf, 6); - push_u32(&mut buf, polygons.0.len() as u32); - for polygon in &polygons.0 { - push_polygon(&mut buf, polygon, xf); + push_u32(&mut buf, wkb_type(6, has_z)); + push_u32(&mut buf, mp.polygons().len() as u32); + for poly in mp.polygons() { + push_polygon(&mut buf, poly, has_z, xf); } } - _ => return Err(MltError::NotImplemented("unsupported geometry type")), + Wkt::GeometryCollection(_) => { + return Err(MltError::NotImplemented("unsupported geometry type")); + } } Ok(buf) } @@ -469,4 +476,33 @@ mod tests { "line fixture should produce WKB type 2 (LineString)" ); } + + /// A 3D geometry must emit ISO WKB with the Z coordinate preserved (type `base + 1000`). + #[test] + fn wkb_preserves_z_for_3d_linestring() { + use mlt_core::wkt::Wkt; + use mlt_core::wkt::types::{Coord, Dimension, LineString}; + + let z = |x, y, z| Coord { + x, + y, + z: Some(z), + m: None, + }; + let ls = Wkt::LineString(LineString::new( + vec![z(1, 2, 3), z(4, 5, 6)], + Dimension::XYZ, + )); + let geom = GeometryValues::default().with_geom(&ls); + + let wkb = geom32_to_wkb(&geom.to_geojson(0).unwrap(), None).expect("wkb"); + + // byte order (1) + type (4) + count (4) + 2 coords * 3 components * 8 bytes. + assert_eq!(wkb.len(), 1 + 4 + 4 + 2 * 3 * 8); + let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]); + assert_eq!(wkb_type, 1002, "3D LineString -> ISO WKB type 1002"); + // The first coordinate's Z (third f64, after x and y) must be 3.0. + let z0 = f64::from_le_bytes(wkb[9 + 16..9 + 24].try_into().unwrap()); + assert_eq!(z0, 3.0); + } } diff --git a/rust/mlt/src/ui/mbt.rs b/rust/mlt/src/ui/mbt.rs index d1e2e2f12..d4a4c1c45 100644 --- a/rust/mlt/src/ui/mbt.rs +++ b/rust/mlt/src/ui/mbt.rs @@ -7,9 +7,10 @@ use std::thread; use martin_tile_utils::{decode_gzip, decode_zstd}; use mbtiles::Mbtiles; -use mlt_core::geo_types::{Coord, Geometry, Polygon}; use mlt_core::geojson::FeatureCollection; use mlt_core::mvt::mvt_to_feature_collection; +use mlt_core::wkt::Wkt; +use mlt_core::wkt::types::{Coord, Polygon}; use rstar::{AABB, PointDistance, RTree, RTreeObject}; use super::group_by_layer; @@ -111,32 +112,43 @@ impl TileTransform { /// Collect world-coordinate vertices from a polygon. pub(crate) fn poly_verts(&self, poly: &Polygon) -> Vec<[f64; 2]> { - poly.exterior() - .0 + poly.rings() .iter() - .copied() - .chain(poly.interiors().iter().flat_map(|r| r.0.iter().copied())) + .flat_map(|r| r.coords().iter().copied()) .map(|c| self.to_world(c)) .collect() } /// Collect world-coordinate vertices from any geometry. - pub(crate) fn geom_verts(&self, geom: &Geometry) -> Vec<[f64; 2]> { + pub(crate) fn geom_verts(&self, geom: &Wkt) -> Vec<[f64; 2]> { match geom { - Geometry::::Point(p) => vec![self.to_world(p.0)], - Geometry::::LineString(ls) => { - ls.0.iter().copied().map(|c| self.to_world(c)).collect() - } - Geometry::::MultiPoint(mp) => mp.iter().map(|p| self.to_world(p.0)).collect(), - Geometry::::Polygon(poly) => self.poly_verts(poly), - Geometry::::MultiLineString(mls) => mls + Wkt::Point(p) => p + .coord() + .map(|c| vec![self.to_world(*c)]) + .unwrap_or_default(), + Wkt::LineString(ls) => ls + .coords() .iter() - .flat_map(|ls| ls.0.iter().copied().map(|c| self.to_world(c))) + .copied() + .map(|c| self.to_world(c)) .collect(), - Geometry::::MultiPolygon(mpoly) => { - mpoly.iter().flat_map(|p| self.poly_verts(p)).collect() - } - _ => vec![], + Wkt::MultiPoint(mp) => mp + .points() + .iter() + .filter_map(|p| p.coord().map(|c| self.to_world(*c))) + .collect(), + Wkt::Polygon(poly) => self.poly_verts(poly), + Wkt::MultiLineString(mls) => mls + .line_strings() + .iter() + .flat_map(|ls| ls.coords().iter().copied().map(|c| self.to_world(c))) + .collect(), + Wkt::MultiPolygon(mpoly) => mpoly + .polygons() + .iter() + .flat_map(|p| self.poly_verts(p)) + .collect(), + Wkt::GeometryCollection(_) => vec![], } } } diff --git a/rust/mlt/src/ui/mod.rs b/rust/mlt/src/ui/mod.rs index 6e524e398..8f05ce11f 100644 --- a/rust/mlt/src/ui/mod.rs +++ b/rust/mlt/src/ui/mod.rs @@ -20,9 +20,10 @@ use crossterm::event::{ MouseButton, MouseEventKind, }; use crossterm::execute; -use mlt_core::geo_types::{Coord, Geometry, Polygon}; use mlt_core::geojson::FeatureCollection; use mlt_core::mvt::mvt_to_feature_collection; +use mlt_core::wkt::Wkt; +use mlt_core::wkt::types::{Coord, Polygon}; use mlt_core::{Decoder, GeometryType, Parser}; use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect}; use ratatui::style::{Color, Modifier, Style}; @@ -1140,52 +1141,47 @@ fn collect_extensions(files: &[LsRow]) -> Vec { // --- Geometry helpers --- -fn geometry_type_name(geom: &Geometry) -> &'static str { +fn geometry_type_name(geom: &Wkt) -> &'static str { GeometryType::try_from(geom).map_or("Unknown", Into::into) } -fn geometry_color(geom: &Geometry) -> Color { +fn geometry_color(geom: &Wkt) -> Color { match geom { - Geometry::::MultiPoint(_) => CLR_MULTI_POINT, - Geometry::::LineString(_) => CLR_LINE, - Geometry::::MultiLineString(_) => CLR_MULTI_LINE, - Geometry::::Polygon(_) | Geometry::::MultiPolygon(_) if has_bad_winding(geom) => { - CLR_BAD_WINDING - } - Geometry::::Polygon(_) => CLR_POLYGON, - Geometry::::MultiPolygon(_) => CLR_MULTI_POLYGON, - Geometry::::Point(_) - | Geometry::::Line(_) - | Geometry::::GeometryCollection(_) - | Geometry::::Rect(_) - | Geometry::::Triangle(_) => CLR_POINT, + Wkt::MultiPoint(_) => CLR_MULTI_POINT, + Wkt::LineString(_) => CLR_LINE, + Wkt::MultiLineString(_) => CLR_MULTI_LINE, + Wkt::Polygon(_) | Wkt::MultiPolygon(_) if has_bad_winding(geom) => CLR_BAD_WINDING, + Wkt::Polygon(_) => CLR_POLYGON, + Wkt::MultiPolygon(_) => CLR_MULTI_POLYGON, + Wkt::Point(_) | Wkt::GeometryCollection(_) => CLR_POINT, } } -fn multi_part_count(geom: &Geometry) -> usize { +fn multi_part_count(geom: &Wkt) -> usize { match geom { - Geometry::::MultiPoint(mp) => mp.0.len(), - Geometry::::MultiLineString(mls) => mls.0.len(), - Geometry::::MultiPolygon(mpoly) => mpoly.0.len(), + Wkt::MultiPoint(mp) => mp.points().len(), + Wkt::MultiLineString(mls) => mls.line_strings().len(), + Wkt::MultiPolygon(mpoly) => mpoly.polygons().len(), _ => 0, } } +/// `(total vertices, ring count)`; first ring is exterior, the rest interiors. fn poly_ring_stats(poly: &Polygon) -> (usize, usize) { - let ring_count = 1 + poly.interiors().len(); - let total_verts = - poly.exterior().0.len() + poly.interiors().iter().map(|r| r.0.len()).sum::(); + let rings = poly.rings(); + let ring_count = rings.len(); + let total_verts = rings.iter().map(|r| r.coords().len()).sum::(); (total_verts, ring_count) } -fn feature_suffix(geom: &Geometry) -> String { +fn feature_suffix(geom: &Wkt) -> String { let n = multi_part_count(geom); if n > 0 { return format!(" ({n} parts)"); } match geom { - Geometry::::LineString(ls) => format!(" ({}v)", ls.0.len()), - Geometry::::Polygon(poly) => { + Wkt::LineString(ls) => format!(" ({}v)", ls.coords().len()), + Wkt::Polygon(poly) => { let (total, ring_count) = poly_ring_stats(poly); if ring_count > 1 { format!(" ({total}v, {ring_count} rings)") @@ -1197,13 +1193,13 @@ fn feature_suffix(geom: &Geometry) -> String { } } -fn sub_feature_suffix(geom: &Geometry, part: usize) -> String { +fn sub_feature_suffix(geom: &Wkt, part: usize) -> String { match geom { - Geometry::::MultiLineString(mls) => mls - .0 + Wkt::MultiLineString(mls) => mls + .line_strings() .get(part) - .map_or(String::new(), |ls| format!(" ({}v)", ls.0.len())), - Geometry::::MultiPolygon(mpoly) => mpoly.0.get(part).map_or(String::new(), |poly| { + .map_or(String::new(), |ls| format!(" ({}v)", ls.coords().len())), + Wkt::MultiPolygon(mpoly) => mpoly.polygons().get(part).map_or(String::new(), |poly| { let (total, ring_count) = poly_ring_stats(poly); if ring_count > 1 { format!(" ({total}v, {ring_count} rings)") @@ -1259,13 +1255,16 @@ pub(crate) fn is_ring_ccw(ring: &[Coord]) -> bool { ring_signed_area(ring) < 0.0 } -fn has_bad_winding(geom: &Geometry) -> bool { +fn has_bad_winding(geom: &Wkt) -> bool { let check = |poly: &Polygon| { - !is_ring_ccw(&poly.exterior().0) || poly.interiors().iter().any(|r| is_ring_ccw(&r.0)) + let rings = poly.rings(); + let exterior_bad = rings.first().is_some_and(|r| !is_ring_ccw(r.coords())); + let interior_bad = rings.iter().skip(1).any(|r| is_ring_ccw(r.coords())); + exterior_bad || interior_bad }; match geom { - Geometry::::Polygon(poly) => check(poly), - Geometry::::MultiPolygon(mpoly) => mpoly.iter().any(check), + Wkt::Polygon(poly) => check(poly), + Wkt::MultiPolygon(mpoly) => mpoly.polygons().iter().any(check), _ => false, } } diff --git a/rust/mlt/src/ui/rendering/layers.rs b/rust/mlt/src/ui/rendering/layers.rs index 78fa478bc..4e4dcb6e7 100644 --- a/rust/mlt/src/ui/rendering/layers.rs +++ b/rust/mlt/src/ui/rendering/layers.rs @@ -1,7 +1,6 @@ -use mlt_core::geo_types::{ - Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, -}; use mlt_core::geojson::Feature; +use mlt_core::wkt::Wkt; +use mlt_core::wkt::types::{LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon}; use ratatui::Frame; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::prelude::{Line, Modifier, Span, Style}; @@ -60,9 +59,9 @@ pub fn render_tree_panel(f: &mut Frame<'_>, area: Rect, app: &mut App) { TreeItem::SubFeature { layer, feat, part } => { let geom = &app.feature(*layer, *feat).geometry; let n = match geom { - Geometry::::MultiPoint(_) => "Point", - Geometry::::MultiLineString(_) => "LineString", - Geometry::::MultiPolygon(_) => "Polygon", + Wkt::MultiPoint(_) => "Point", + Wkt::MultiLineString(_) => "LineString", + Wkt::MultiPolygon(_) => "Polygon", _ => "Part", }; ( @@ -197,89 +196,85 @@ fn render_properties_top(f: &mut Frame<'_>, area: Rect, app: &mut App) { f.render_widget(para, area); } -fn info_point(lines: &mut Vec>, p: Point) { - lines.push(stat_line("Coords", &format!("{:?}", <[i32; 2]>::from(p)))); +fn info_point(lines: &mut Vec>, p: &Point) { + let coords = p + .coord() + .map_or_else(|| "[]".to_string(), |c| format!("{:?}", [c.x, c.y])); + lines.push(stat_line("Coords", &coords)); } fn info_line_string(lines: &mut Vec>, ls: &LineString) { - lines.push(stat_line("Vertices", &ls.0.len())); + lines.push(stat_line("Vertices", &ls.coords().len())); } fn info_polygon(lines: &mut Vec>, poly: &Polygon) { - let total: usize = - poly.exterior().0.len() + poly.interiors().iter().map(|r| r.0.len()).sum::(); + let rings = poly.rings(); + let total: usize = rings.iter().map(|r| r.coords().len()).sum(); lines.push(stat_line("Vertices", &total)); - lines.push(stat_line("Rings", &(1 + poly.interiors().len()))); - let ext = &poly.exterior().0; - let w = if is_ring_ccw(ext) { "CCW" } else { "CW" }; - lines.push(Line::from(format!(" Ring 0: {}v, {w}", ext.len()))); - for (i, ring) in poly.interiors().iter().enumerate() { - let w = if is_ring_ccw(&ring.0) { "CCW" } else { "CW" }; - lines.push(Line::from(format!( - " Ring {}: {}v, {w}", - i + 1, - ring.0.len() - ))); + lines.push(stat_line("Rings", &rings.len())); + for (i, ring) in rings.iter().enumerate() { + let coords = ring.coords(); + let w = if is_ring_ccw(coords) { "CCW" } else { "CW" }; + lines.push(Line::from(format!(" Ring {}: {}v, {w}", i, coords.len()))); } } fn info_multi_point(lines: &mut Vec>, pts: &MultiPoint) { - lines.push(stat_line("Points", &pts.0.len())); + lines.push(stat_line("Points", &pts.points().len())); } fn info_multi_line_string(lines: &mut Vec>, mls: &MultiLineString) { - let total: usize = mls.iter().map(|ls| ls.0.len()).sum(); - lines.push(stat_line("Parts", &mls.0.len())); + let total: usize = mls.line_strings().iter().map(|ls| ls.coords().len()).sum(); + lines.push(stat_line("Parts", &mls.line_strings().len())); lines.push(stat_line("Vertices", &total)); } fn info_multi_polygon(lines: &mut Vec>, mpoly: &MultiPolygon) { - let total: usize = mpoly + let polys = mpoly.polygons(); + let total: usize = polys .iter() - .flat_map(|p| { - std::iter::once(p.exterior().0.len()).chain(p.interiors().iter().map(|r| r.0.len())) - }) + .flat_map(|p| p.rings().iter().map(|r| r.coords().len())) .sum(); - let total_rings: usize = mpoly.iter().map(|p| 1 + p.interiors().len()).sum(); - lines.push(stat_line("Parts", &mpoly.0.len())); + let total_rings: usize = polys.iter().map(|p| p.rings().len()).sum(); + lines.push(stat_line("Parts", &polys.len())); lines.push(stat_line("Total vertices", &total)); lines.push(stat_line("Total rings", &total_rings)); } -fn geometry_stats_lines(geom: &Geometry) -> Vec> { +fn geometry_stats_lines(geom: &Wkt) -> Vec> { let mut lines = vec![stat_line("Type", &geometry_type_name(geom))]; match geom { - Geometry::::Point(p) => info_point(&mut lines, *p), - Geometry::::LineString(ls) => info_line_string(&mut lines, ls), - Geometry::::Polygon(poly) => info_polygon(&mut lines, poly), - Geometry::::MultiPoint(pts) => info_multi_point(&mut lines, pts), - Geometry::::MultiLineString(mls) => info_multi_line_string(&mut lines, mls), - Geometry::::MultiPolygon(mpoly) => info_multi_polygon(&mut lines, mpoly), - _ => unreachable!("Unexpected geometry type {geom:?}"), + Wkt::Point(p) => info_point(&mut lines, p), + Wkt::LineString(ls) => info_line_string(&mut lines, ls), + Wkt::Polygon(poly) => info_polygon(&mut lines, poly), + Wkt::MultiPoint(pts) => info_multi_point(&mut lines, pts), + Wkt::MultiLineString(mls) => info_multi_line_string(&mut lines, mls), + Wkt::MultiPolygon(mpoly) => info_multi_polygon(&mut lines, mpoly), + Wkt::GeometryCollection(_) => unreachable!("Unexpected geometry type {geom:?}"), } lines } -fn subpart_stats_lines(geom: &Geometry, part: usize) -> Vec> { +fn subpart_stats_lines(geom: &Wkt, part: usize) -> Vec> { let mut lines = vec![stat_line( "Component", &format!("part #{} of a {}", part, geometry_type_name(geom)), )]; match geom { - Geometry::::MultiPoint(pts) => { - if let Some(p) = pts.0.get(part) { + Wkt::MultiPoint(pts) => { + if let Some(p) = pts.points().get(part) { lines.push(stat_line("Type", &"Point")); - info_point(&mut lines, *p); + info_point(&mut lines, p); } } - Geometry::::MultiLineString(mls) => { - if let Some(ls) = mls.0.get(part) { + Wkt::MultiLineString(mls) => { + if let Some(ls) = mls.line_strings().get(part) { lines.push(stat_line("Type", &"LineString")); info_line_string(&mut lines, ls); } } - Geometry::::MultiPolygon(mpoly) => { - if let Some(poly) = mpoly.0.get(part) { + Wkt::MultiPolygon(mpoly) => { + if let Some(poly) = mpoly.polygons().get(part) { lines.push(stat_line("Type", &"Polygon")); info_polygon(&mut lines, poly); } diff --git a/rust/mlt/src/ui/rendering/map.rs b/rust/mlt/src/ui/rendering/map.rs index 81677b87e..676e1507e 100644 --- a/rust/mlt/src/ui/rendering/map.rs +++ b/rust/mlt/src/ui/rendering/map.rs @@ -1,7 +1,8 @@ use std::collections::HashSet; -use mlt_core::geo_types::{Coord, Geometry, Polygon}; use mlt_core::geojson::FeatureCollection; +use mlt_core::wkt::Wkt; +use mlt_core::wkt::types::{Coord, Polygon}; use ratatui::Frame; use ratatui::layout::Rect; use ratatui::prelude::{Span, Style}; @@ -102,7 +103,7 @@ pub fn render_tile_preview(f: &mut Frame<'_>, area: Rect, fc: &FeatureCollection fn draw_feature( ctx: &mut Context<'_>, - geom: &Geometry, + geom: &Wkt, base: Color, is_hov: bool, sel_part: Option, @@ -110,26 +111,32 @@ fn draw_feature( ) { let color = if is_hov { CLR_HOVERED } else { base }; match geom { - Geometry::::Point(p) => draw_point(ctx, p.0, color), - Geometry::::LineString(ls) => draw_line(ctx, &ls.0, color), - Geometry::::Polygon(poly) => draw_polygon(ctx, poly, is_hov, color), - Geometry::::MultiPoint(pts) => { - for (i, p) in pts.iter().enumerate() { - draw_point(ctx, p.0, part_color(sel_part, hov_part, i, color)); + Wkt::Point(p) => { + if let Some(c) = p.coord() { + draw_point(ctx, *c, color); } } - Geometry::::MultiLineString(lines) => { - for (i, ls) in lines.iter().enumerate() { - draw_line(ctx, &ls.0, part_color(sel_part, hov_part, i, color)); + Wkt::LineString(ls) => draw_line(ctx, ls.coords(), color), + Wkt::Polygon(poly) => draw_polygon(ctx, poly, is_hov, color), + Wkt::MultiPoint(pts) => { + for (i, p) in pts.points().iter().enumerate() { + if let Some(c) = p.coord() { + draw_point(ctx, *c, part_color(sel_part, hov_part, i, color)); + } + } + } + Wkt::MultiLineString(lines) => { + for (i, ls) in lines.line_strings().iter().enumerate() { + draw_line(ctx, ls.coords(), part_color(sel_part, hov_part, i, color)); } } - Geometry::::MultiPolygon(polys) => { - for (i, poly) in polys.iter().enumerate() { + Wkt::MultiPolygon(polys) => { + for (i, poly) in polys.polygons().iter().enumerate() { let pc = part_color(sel_part, hov_part, i, color); draw_polygon(ctx, poly, matches!(pc, CLR_HOVERED | CLR_SELECTED), pc); } } - _ => {} + Wkt::GeometryCollection(_) => {} } } @@ -170,10 +177,9 @@ fn ring_color(ring: &[Coord], highlighted: bool, fallback: Color) -> Color } fn draw_polygon(ctx: &mut Context<'_>, poly: &Polygon, highlighted: bool, fallback: Color) { - let ext = &poly.exterior().0; - draw_ring(ctx, ext, ring_color(ext, highlighted, fallback)); - for ring in poly.interiors() { - draw_ring(ctx, &ring.0, ring_color(&ring.0, highlighted, fallback)); + for ring in poly.rings() { + let coords = ring.coords(); + draw_ring(ctx, coords, ring_color(coords, highlighted, fallback)); } } @@ -352,46 +358,49 @@ fn draw_world_rect_vp( /// Draw a geometry in world coordinates using the provided tile transform (north-up on canvas). fn draw_geom_world( ctx: &mut Context<'_>, - geom: &Geometry, + geom: &Wkt, t: &TileTransform, vp_y0: f64, vp_y1: f64, color: Color, ) { + let print_point = |ctx: &mut Context<'_>, c: Coord| { + let [wx, wy] = t.to_world(c); + let sy = mbt_screen_y(vp_y0, vp_y1, wy); + ctx.print(wx, sy, Span::styled("×", Style::default().fg(color))); + }; match geom { - Geometry::::Point(p) => { - let [wx, wy] = t.to_world(p.0); - let sy = mbt_screen_y(vp_y0, vp_y1, wy); - ctx.print(wx, sy, Span::styled("×", Style::default().fg(color))); + Wkt::Point(p) => { + if let Some(c) = p.coord() { + print_point(ctx, *c); + } } - Geometry::::LineString(ls) => draw_world_line(ctx, &ls.0, t, vp_y0, vp_y1, color), - Geometry::::Polygon(poly) => { - draw_world_ring(ctx, &poly.exterior().0, t, vp_y0, vp_y1, color); - for ring in poly.interiors() { - draw_world_ring(ctx, &ring.0, t, vp_y0, vp_y1, color); + Wkt::LineString(ls) => draw_world_line(ctx, ls.coords(), t, vp_y0, vp_y1, color), + Wkt::Polygon(poly) => { + for ring in poly.rings() { + draw_world_ring(ctx, ring.coords(), t, vp_y0, vp_y1, color); } } - Geometry::::MultiPoint(mp) => { - for p in mp.iter() { - let [wx, wy] = t.to_world(p.0); - let sy = mbt_screen_y(vp_y0, vp_y1, wy); - ctx.print(wx, sy, Span::styled("×", Style::default().fg(color))); + Wkt::MultiPoint(mp) => { + for p in mp.points() { + if let Some(c) = p.coord() { + print_point(ctx, *c); + } } } - Geometry::::MultiLineString(mls) => { - for ls in mls.iter() { - draw_world_line(ctx, &ls.0, t, vp_y0, vp_y1, color); + Wkt::MultiLineString(mls) => { + for ls in mls.line_strings() { + draw_world_line(ctx, ls.coords(), t, vp_y0, vp_y1, color); } } - Geometry::::MultiPolygon(mpoly) => { - for poly in mpoly.iter() { - draw_world_ring(ctx, &poly.exterior().0, t, vp_y0, vp_y1, color); - for ring in poly.interiors() { - draw_world_ring(ctx, &ring.0, t, vp_y0, vp_y1, color); + Wkt::MultiPolygon(mpoly) => { + for poly in mpoly.polygons() { + for ring in poly.rings() { + draw_world_ring(ctx, ring.coords(), t, vp_y0, vp_y1, color); } } } - _ => {} + Wkt::GeometryCollection(_) => {} } } diff --git a/rust/mlt/src/ui/state.rs b/rust/mlt/src/ui/state.rs index e0393d6b8..43ae8cb5b 100644 --- a/rust/mlt/src/ui/state.rs +++ b/rust/mlt/src/ui/state.rs @@ -4,8 +4,9 @@ use std::sync::mpsc; use std::time::Instant; use mlt_core::GeometryType; -use mlt_core::geo_types::{Geometry, Polygon}; use mlt_core::geojson::{Feature, FeatureCollection}; +use mlt_core::wkt::Wkt; +use mlt_core::wkt::types::Polygon; use ratatui::layout::{Constraint, Rect}; use ratatui::widgets::TableState; use rstar::{PointDistance as _, RTree}; @@ -698,7 +699,7 @@ impl App { y1 = y1.max(v[1]); }; - let geoms: Vec<&Geometry> = match sel { + let geoms: Vec<&Wkt> = match sel { TreeItem::All => self.fc.features.iter().map(|f| &f.geometry).collect(), TreeItem::Layer(l) => self.layer_groups[*l] .feature_indices @@ -890,38 +891,41 @@ fn file_cmp(a: &LsRow, b: &LsRow, col: FileSortColumn, asc: bool) -> std::cmp::O } fn poly_verts(poly: &Polygon) -> Vec<[f64; 2]> { - poly.exterior() - .0 + poly.rings() .iter() - .copied() - .chain(poly.interiors().iter().flat_map(|r| r.0.iter().copied())) + .flat_map(|r| r.coords().iter().copied()) .map(coord_f64) .collect() } -fn geometry_vertices(geom: &Geometry, part: Option) -> Vec<[f64; 2]> { +fn geometry_vertices(geom: &Wkt, part: Option) -> Vec<[f64; 2]> { match (geom, part) { - (Geometry::::Point(p), None) => vec![coord_f64(p.0)], - (Geometry::::LineString(ls), None) => ls.0.iter().copied().map(coord_f64).collect(), - (Geometry::::MultiPoint(mp), None) => mp.iter().map(|p| coord_f64(p.0)).collect(), - (Geometry::::Polygon(poly), None) => poly_verts(poly), - (Geometry::::MultiLineString(mls), None) => mls + (Wkt::Point(p), None) => p.coord().map(|c| vec![coord_f64(*c)]).unwrap_or_default(), + (Wkt::LineString(ls), None) => ls.coords().iter().copied().map(coord_f64).collect(), + (Wkt::MultiPoint(mp), None) => mp + .points() .iter() - .flat_map(|ls| ls.0.iter().copied().map(coord_f64)) + .filter_map(|p| p.coord().map(|c| coord_f64(*c))) .collect(), - (Geometry::::MultiPolygon(mpoly), None) => mpoly.iter().flat_map(poly_verts).collect(), - (Geometry::::MultiPoint(mp), Some(p)) => { - mp.0.get(p) - .map(|pt| vec![coord_f64(pt.0)]) - .unwrap_or_default() - } - (Geometry::::MultiLineString(mls), Some(p)) => mls - .0 + (Wkt::Polygon(poly), None) => poly_verts(poly), + (Wkt::MultiLineString(mls), None) => mls + .line_strings() + .iter() + .flat_map(|ls| ls.coords().iter().copied().map(coord_f64)) + .collect(), + (Wkt::MultiPolygon(mpoly), None) => mpoly.polygons().iter().flat_map(poly_verts).collect(), + (Wkt::MultiPoint(mp), Some(p)) => mp + .points() + .get(p) + .and_then(|pt| pt.coord().map(|c| vec![coord_f64(*c)])) + .unwrap_or_default(), + (Wkt::MultiLineString(mls), Some(p)) => mls + .line_strings() .get(p) - .map(|ls| ls.0.iter().copied().map(coord_f64).collect()) + .map(|ls| ls.coords().iter().copied().map(coord_f64).collect()) .unwrap_or_default(), - (Geometry::::MultiPolygon(mpoly), Some(p)) => { - mpoly.0.get(p).map(poly_verts).unwrap_or_default() + (Wkt::MultiPolygon(mpoly), Some(p)) => { + mpoly.polygons().get(p).map(poly_verts).unwrap_or_default() } _ => Vec::new(), }