Skip to main content

mlt_core/encoder/
model.rs

1use std::borrow::Cow;
2use std::collections::HashSet;
3
4use derive_debug::Dbg;
5
6use crate::decoder::{DictionaryType, GeometryValues, RleLayout, StreamType};
7use crate::encoder::geometry::VertexBufferType;
8use crate::encoder::{IntEncoder, StagedId, StagedProperty};
9use crate::tile::Extent;
10use crate::{MltError, MltResult};
11
12/// Owned variant of `Unknown`.
13#[derive(Debug, Clone, Default, PartialEq)]
14pub struct EncodedUnknown {
15    pub(crate) tag: u8,
16    pub(crate) value: Vec<u8>,
17}
18
19impl EncodedUnknown {
20    pub fn new(tag: u8, value: Vec<u8>) -> MltResult<Self> {
21        if tag == 1 {
22            return Err(MltError::ParsingColumnType(tag));
23        }
24        Ok(Self { tag, value })
25    }
26
27    #[must_use]
28    pub fn tag(&self) -> u32 {
29        u32::from(self.tag)
30    }
31
32    #[must_use]
33    pub fn data(&self) -> &[u8] {
34        &self.value
35    }
36}
37
38/// Parameters derived from the vertex set of a feature collection, used to
39/// normalize coordinates before space-filling-curve key computation.
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct CurveParams {
42    pub shift: u32,
43    pub bits: u32,
44}
45
46impl Default for CurveParams {
47    fn default() -> Self {
48        Self { shift: 0, bits: 1 }
49    }
50}
51
52impl CurveParams {
53    /// Compute params from a flat `[x0, y0, x1, y1, …]` vertex slice.
54    #[must_use]
55    pub fn from_vertices(vertices: &[i32]) -> Self {
56        if vertices.is_empty() {
57            return Self::default();
58        }
59        let (min, max) = vertices
60            .iter()
61            .fold((i32::MAX, i32::MIN), |(mn, mx), &v| (mn.min(v), mx.max(v)));
62        crate::codecs::hilbert::hilbert_curve_params_from_bounds(min, max)
63    }
64}
65
66/// Columnar layer data being prepared for encoding (stage 2 of the encoding pipeline).
67///
68/// Holds fully-owned columnar data. Constructed directly (synthetics, benches) or
69/// converted from [`TileLayer`](crate::TileLayer).
70/// Consumed by encoding via [`StagedLayer::encode_into`] or `StagedLayer::encode_explicit`
71/// (with explicit encoding mode enabled).
72#[derive(Debug, PartialEq, Clone)]
73pub struct StagedLayer {
74    pub(crate) name: String,
75    pub(crate) extent: Extent,
76    pub(crate) id: StagedId,
77    pub(crate) geometry: GeometryValues,
78    pub(crate) properties: Vec<StagedProperty>,
79}
80
81#[cfg_attr(not(feature = "__private"), allow(dead_code))]
82impl StagedLayer {
83    pub fn new(
84        name: impl Into<String>,
85        extent: u32,
86        id: StagedId,
87        geometry: GeometryValues,
88        properties: Vec<StagedProperty>,
89    ) -> MltResult<Self> {
90        let name = name.into();
91        if name.is_empty() {
92            return Err(MltError::MissingLayerName);
93        }
94        let extent = Extent::new(extent)?;
95        let feature_count = geometry.feature_count();
96        if let Some(actual) = id.feature_count()
97            && actual != feature_count
98        {
99            return Err(MltError::StagedFeatureCountMismatch {
100                column: "id".into(),
101                expected: feature_count,
102                actual,
103            });
104        }
105        // Column names must be unique within a layer. A shared dictionary's `name()` is
106        // only its prefix (which may repeat); its real columns are `{prefix}{suffix}`.
107        // Scoped so `seen` releases its borrow of `properties` before the move below.
108        {
109            let mut seen: HashSet<Cow<str>> = HashSet::new();
110            for property in &properties {
111                let actual = property.feature_count();
112                if actual != feature_count {
113                    return Err(MltError::StagedFeatureCountMismatch {
114                        column: property.name().to_string(),
115                        expected: feature_count,
116                        actual,
117                    });
118                }
119                if let StagedProperty::SharedDict(sd) = property {
120                    for item in &sd.items {
121                        if !seen.insert(Cow::Owned(format!("{}{}", sd.prefix, item.suffix))) {
122                            return Err(MltError::DuplicatePropertyName(format!(
123                                "{}{}",
124                                sd.prefix, item.suffix
125                            )));
126                        }
127                    }
128                } else if !seen.insert(Cow::Borrowed(property.name())) {
129                    return Err(MltError::DuplicatePropertyName(property.name().to_string()));
130                }
131            }
132        }
133        Ok(Self {
134            name,
135            extent,
136            id,
137            geometry,
138            properties,
139        })
140    }
141
142    #[must_use]
143    pub fn name(&self) -> &str {
144        &self.name
145    }
146
147    #[must_use]
148    pub fn extent(&self) -> Extent {
149        self.extent
150    }
151
152    #[must_use]
153    pub fn id(&self) -> &StagedId {
154        &self.id
155    }
156
157    #[must_use]
158    pub fn geometry(&self) -> &GeometryValues {
159        &self.geometry
160    }
161
162    #[must_use]
163    pub fn properties(&self) -> &[StagedProperty] {
164        &self.properties
165    }
166}
167
168/// Which wire format layers are encoded to.
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
170#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
171pub enum WireVersion {
172    /// Tag `0x01` - the stable v1 format.
173    #[default]
174    V01,
175    /// Tag `0x02` - the experimental v2 format (see `docs/migrating-to-v2.md`).
176    /// Requires the `unstable-v2` feature.
177    ///
178    /// Currently limited to ID, scalar, and non-tessellated geometry columns;
179    /// string and shared-dictionary columns are not yet supported.
180    #[cfg(feature = "unstable-v2")]
181    V02,
182}
183
184impl WireVersion {
185    /// The layer tag byte identifying this format on the wire.
186    #[must_use]
187    pub(crate) fn tag(self) -> u8 {
188        match self {
189            Self::V01 => 1,
190            #[cfg(feature = "unstable-v2")]
191            Self::V02 => 2,
192        }
193    }
194
195    /// The RLE stream data layout used by this format.
196    #[must_use]
197    pub(crate) fn rle_layout(self) -> RleLayout {
198        match self {
199            Self::V01 => RleLayout::Split,
200            #[cfg(feature = "unstable-v2")]
201            Self::V02 => RleLayout::Interleaved,
202        }
203    }
204}
205
206/// Global encoder settings controlling which optimization strategies are attempted.
207#[derive(Debug, Clone, Copy, PartialEq, Hash)]
208#[expect(
209    clippy::struct_excessive_bools,
210    reason = "enums would not model this better, not a state machine"
211)]
212pub struct EncoderConfig {
213    /// The wire format to encode layers to.
214    wire_version: WireVersion,
215    /// Generate tessellation data for polygons and multi-polygons.
216    tessellate: bool,
217    /// Try sorting features by the Z-order (Morton) curve index of their first vertex.
218    attempt_spatial_morton_sort: bool,
219    /// Try sorting features by the Hilbert curve index of their first vertex.
220    attempt_spatial_hilbert_sort: bool,
221    /// Try sorting features by their feature ID in ascending order.
222    attempt_id_sort: bool,
223    /// Allow `FSST` string compression
224    allow_fsst: bool,
225    /// Allow `FastPFOR` integer compression
226    allow_fastpfor: bool,
227    /// Allow string grouping into shared dictionaries
228    allow_shared_dict: bool,
229}
230impl Default for EncoderConfig {
231    fn default() -> Self {
232        Self {
233            wire_version: WireVersion::V01,
234            tessellate: false,
235            attempt_spatial_morton_sort: true,
236            attempt_spatial_hilbert_sort: true,
237            attempt_id_sort: true,
238            allow_fsst: true,
239            allow_fastpfor: true,
240            allow_shared_dict: true,
241        }
242    }
243}
244
245impl EncoderConfig {
246    #[must_use]
247    pub fn wire_version(self) -> WireVersion {
248        self.wire_version
249    }
250
251    #[must_use]
252    pub fn tessellate(self) -> bool {
253        self.tessellate
254    }
255
256    #[must_use]
257    pub fn attempt_spatial_morton_sort(self) -> bool {
258        self.attempt_spatial_morton_sort
259    }
260
261    #[must_use]
262    pub fn attempt_spatial_hilbert_sort(self) -> bool {
263        self.attempt_spatial_hilbert_sort
264    }
265
266    #[must_use]
267    pub fn attempt_id_sort(self) -> bool {
268        self.attempt_id_sort
269    }
270
271    #[must_use]
272    pub fn allow_fsst(self) -> bool {
273        self.allow_fsst
274    }
275
276    #[must_use]
277    pub fn allow_fastpfor(self) -> bool {
278        // TODO(v2): race FastPFor128-LE for `WireVersion::V02`.
279        // v2 will use `FastPFor128` in little-endian byte order.
280        // Until that codec lands, `FastPFor` is only attempted for v1 layers.
281        self.allow_fastpfor && self.wire_version == WireVersion::V01
282    }
283
284    #[must_use]
285    pub fn allow_shared_dict(self) -> bool {
286        self.allow_shared_dict
287    }
288
289    #[must_use]
290    pub fn with_wire_version(mut self, version: WireVersion) -> Self {
291        self.wire_version = version;
292        self
293    }
294
295    #[must_use]
296    pub fn with_tessellation(mut self, enabled: bool) -> Self {
297        self.tessellate = enabled;
298        self
299    }
300
301    #[must_use]
302    pub fn with_spatial_morton_sort(mut self, enabled: bool) -> Self {
303        self.attempt_spatial_morton_sort = enabled;
304        self
305    }
306
307    #[must_use]
308    pub fn with_spatial_hilbert_sort(mut self, enabled: bool) -> Self {
309        self.attempt_spatial_hilbert_sort = enabled;
310        self
311    }
312
313    #[must_use]
314    pub fn with_id_sort(mut self, enabled: bool) -> Self {
315        self.attempt_id_sort = enabled;
316        self
317    }
318
319    #[must_use]
320    pub fn with_fsst(mut self, enabled: bool) -> Self {
321        self.allow_fsst = enabled;
322        self
323    }
324
325    #[must_use]
326    pub fn with_fastpfor(mut self, enabled: bool) -> Self {
327        self.allow_fastpfor = enabled;
328        self
329    }
330
331    #[must_use]
332    pub fn with_shared_dict(mut self, enabled: bool) -> Self {
333        self.allow_shared_dict = enabled;
334        self
335    }
336}
337
338/// How to encode a string column.
339///
340/// Used by [`ExplicitEncoder`] to control per-column string encoding in the
341/// explicit (synthetics / `__private`) path and in property-encoding helpers.
342///
343/// Publicly visible only when the `__private` feature is enabled (re-exported from
344/// [`crate::encoder`]).  Always compiled so that the unified property-encoding path
345/// can reference it without feature flags.
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub enum StrEncoding {
348    Plain,
349    Dict,
350    Fsst,
351    FsstDict,
352}
353
354#[derive(Debug, Clone, Copy, PartialEq)]
355pub enum ColumnKind {
356    Id,
357    Geometry,
358    Property,
359}
360
361/// Context for per-stream encoding decisions in [`ExplicitEncoder`] callbacks.
362#[derive(Clone, Copy, Debug, PartialEq)]
363pub struct StreamCtx<'a> {
364    pub kind: ColumnKind,
365    pub stream_type: StreamType,
366    pub name: &'a str,
367    pub subname: &'a str,
368}
369impl<'a> StreamCtx<'a> {
370    /// Stream with a logical sub-part (e.g. string column `"lengths"` / `"offsets"`, shared-dict child suffix).
371    #[inline]
372    #[must_use]
373    pub const fn new(
374        kind: ColumnKind,
375        stream_type: StreamType,
376        name: &'a str,
377        subname: &'a str,
378    ) -> Self {
379        Self {
380            kind,
381            stream_type,
382            name,
383            subname,
384        }
385    }
386
387    #[inline]
388    #[must_use]
389    pub const fn id(stream_type: StreamType) -> Self {
390        Self::new(ColumnKind::Id, stream_type, "", "")
391    }
392
393    #[inline]
394    #[must_use]
395    pub const fn geom(stream_type: StreamType, name: &'a str) -> Self {
396        Self::new(ColumnKind::Geometry, stream_type, name, "")
397    }
398
399    #[inline]
400    #[must_use]
401    pub const fn prop(stream_type: StreamType, name: &'a str) -> Self {
402        Self::new(ColumnKind::Property, stream_type, name, "")
403    }
404
405    #[inline]
406    #[must_use]
407    pub const fn prop_data(name: &'a str) -> Self {
408        let stream_type = StreamType::Data(DictionaryType::None);
409        Self::new(ColumnKind::Property, stream_type, name, "")
410    }
411
412    #[inline]
413    #[must_use]
414    pub const fn prop2(stream_type: StreamType, prefix: &'a str, suffix: &'a str) -> Self {
415        Self::new(ColumnKind::Property, stream_type, prefix, suffix)
416    }
417}
418
419/// Explicit, deterministic encoding configuration for synthetics and tests.
420///
421/// All encoding choices are caller-specified via callbacks so one struct can cover
422/// any combination without per-stream boilerplate.
423///
424/// Always compiled; publicly visible only when the `__private` feature is enabled
425/// (re-exported from [`crate::encoder`]).
426#[derive(Dbg)]
427pub struct ExplicitEncoder {
428    /// Vertex buffer layout for geometry streams.
429    pub vertex_buffer_type: VertexBufferType,
430    /// Per-stream override for the skip-empty-stream rule used by `write_geo_u32_stream`.
431    #[dbg(skip)]
432    pub force_stream: Box<dyn for<'a> Fn(&'a StreamCtx<'a>) -> bool>,
433    /// Return the [`IntEncoder`] for a stream identified by [`StreamCtx`].
434    #[dbg(skip)]
435    pub get_int_encoder: Box<dyn for<'a> Fn(&'a StreamCtx<'a>) -> IntEncoder>,
436    /// Return the string encoding strategy for a string property column.
437    #[dbg(skip)]
438    pub get_str_encoding: Box<dyn Fn(&str) -> StrEncoding>,
439}