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                match property {
120                    StagedProperty::SharedDict(sd) => {
121                        for item in &sd.items {
122                            if !seen.insert(Cow::Owned(format!("{}{}", sd.prefix, item.suffix))) {
123                                return Err(MltError::DuplicatePropertyName(format!(
124                                    "{}{}",
125                                    sd.prefix, item.suffix
126                                )));
127                            }
128                        }
129                    }
130                    _ => {
131                        if !seen.insert(Cow::Borrowed(property.name())) {
132                            return Err(MltError::DuplicatePropertyName(
133                                property.name().to_string(),
134                            ));
135                        }
136                    }
137                }
138            }
139        }
140        Ok(Self {
141            name,
142            extent,
143            id,
144            geometry,
145            properties,
146        })
147    }
148
149    #[must_use]
150    pub fn name(&self) -> &str {
151        &self.name
152    }
153
154    #[must_use]
155    pub fn extent(&self) -> Extent {
156        self.extent
157    }
158
159    #[must_use]
160    pub fn id(&self) -> &StagedId {
161        &self.id
162    }
163
164    #[must_use]
165    pub fn geometry(&self) -> &GeometryValues {
166        &self.geometry
167    }
168
169    #[must_use]
170    pub fn properties(&self) -> &[StagedProperty] {
171        &self.properties
172    }
173}
174
175/// Which wire format layers are encoded to.
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
177#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
178pub enum WireVersion {
179    /// Tag `0x01` - the stable v1 format.
180    #[default]
181    V01,
182    /// Tag `0x02` - the experimental v2 format (see `docs/migrating-to-v2.md`).
183    /// Requires the `unstable-v2` feature.
184    ///
185    /// Currently limited to ID, scalar, and non-tessellated geometry columns;
186    /// string and shared-dictionary columns are not yet supported.
187    #[cfg(feature = "unstable-v2")]
188    V02,
189}
190
191impl WireVersion {
192    /// The layer tag byte identifying this format on the wire.
193    #[must_use]
194    pub(crate) fn tag(self) -> u8 {
195        match self {
196            Self::V01 => 1,
197            #[cfg(feature = "unstable-v2")]
198            Self::V02 => 2,
199        }
200    }
201
202    /// The RLE stream data layout used by this format.
203    #[must_use]
204    pub(crate) fn rle_layout(self) -> RleLayout {
205        match self {
206            Self::V01 => RleLayout::Split,
207            #[cfg(feature = "unstable-v2")]
208            Self::V02 => RleLayout::Interleaved,
209        }
210    }
211}
212
213/// Global encoder settings controlling which optimization strategies are attempted.
214#[derive(Debug, Clone, Copy, PartialEq, Hash)]
215#[expect(
216    clippy::struct_excessive_bools,
217    reason = "enums would not model this better, not a state machine"
218)]
219pub struct EncoderConfig {
220    /// The wire format to encode layers to.
221    wire_version: WireVersion,
222    /// Generate tessellation data for polygons and multi-polygons.
223    tessellate: bool,
224    /// Try sorting features by the Z-order (Morton) curve index of their first vertex.
225    attempt_spatial_morton_sort: bool,
226    /// Try sorting features by the Hilbert curve index of their first vertex.
227    attempt_spatial_hilbert_sort: bool,
228    /// Try sorting features by their feature ID in ascending order.
229    attempt_id_sort: bool,
230    /// Allow `FSST` string compression
231    allow_fsst: bool,
232    /// Allow `FastPFOR` integer compression
233    allow_fastpfor: bool,
234    /// Allow string grouping into shared dictionaries
235    allow_shared_dict: bool,
236}
237impl Default for EncoderConfig {
238    fn default() -> Self {
239        Self {
240            wire_version: WireVersion::V01,
241            tessellate: false,
242            attempt_spatial_morton_sort: true,
243            attempt_spatial_hilbert_sort: true,
244            attempt_id_sort: true,
245            allow_fsst: true,
246            allow_fastpfor: true,
247            allow_shared_dict: true,
248        }
249    }
250}
251
252impl EncoderConfig {
253    #[must_use]
254    pub fn wire_version(self) -> WireVersion {
255        self.wire_version
256    }
257
258    #[must_use]
259    pub fn tessellate(self) -> bool {
260        self.tessellate
261    }
262
263    #[must_use]
264    pub fn attempt_spatial_morton_sort(self) -> bool {
265        self.attempt_spatial_morton_sort
266    }
267
268    #[must_use]
269    pub fn attempt_spatial_hilbert_sort(self) -> bool {
270        self.attempt_spatial_hilbert_sort
271    }
272
273    #[must_use]
274    pub fn attempt_id_sort(self) -> bool {
275        self.attempt_id_sort
276    }
277
278    #[must_use]
279    pub fn allow_fsst(self) -> bool {
280        self.allow_fsst
281    }
282
283    #[must_use]
284    pub fn allow_fastpfor(self) -> bool {
285        // TODO(v2): race FastPFor128-LE for `WireVersion::V02`.
286        // v2 will use `FastPFor128` in little-endian byte order.
287        // Until that codec lands, `FastPFor` is only attempted for v1 layers.
288        self.allow_fastpfor && self.wire_version == WireVersion::V01
289    }
290
291    #[must_use]
292    pub fn allow_shared_dict(self) -> bool {
293        self.allow_shared_dict
294    }
295
296    #[must_use]
297    pub fn with_wire_version(mut self, version: WireVersion) -> Self {
298        self.wire_version = version;
299        self
300    }
301
302    #[must_use]
303    pub fn with_tessellation(mut self, enabled: bool) -> Self {
304        self.tessellate = enabled;
305        self
306    }
307
308    #[must_use]
309    pub fn with_spatial_morton_sort(mut self, enabled: bool) -> Self {
310        self.attempt_spatial_morton_sort = enabled;
311        self
312    }
313
314    #[must_use]
315    pub fn with_spatial_hilbert_sort(mut self, enabled: bool) -> Self {
316        self.attempt_spatial_hilbert_sort = enabled;
317        self
318    }
319
320    #[must_use]
321    pub fn with_id_sort(mut self, enabled: bool) -> Self {
322        self.attempt_id_sort = enabled;
323        self
324    }
325
326    #[must_use]
327    pub fn with_fsst(mut self, enabled: bool) -> Self {
328        self.allow_fsst = enabled;
329        self
330    }
331
332    #[must_use]
333    pub fn with_fastpfor(mut self, enabled: bool) -> Self {
334        self.allow_fastpfor = enabled;
335        self
336    }
337
338    #[must_use]
339    pub fn with_shared_dict(mut self, enabled: bool) -> Self {
340        self.allow_shared_dict = enabled;
341        self
342    }
343}
344
345/// How to encode a string column.
346///
347/// Used by [`ExplicitEncoder`] to control per-column string encoding in the
348/// explicit (synthetics / `__private`) path and in property-encoding helpers.
349///
350/// Publicly visible only when the `__private` feature is enabled (re-exported from
351/// [`crate::encoder`]).  Always compiled so that the unified property-encoding path
352/// can reference it without feature flags.
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum StrEncoding {
355    Plain,
356    Dict,
357    Fsst,
358    FsstDict,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq)]
362pub enum ColumnKind {
363    Id,
364    Geometry,
365    Property,
366}
367
368/// Context for per-stream encoding decisions in [`ExplicitEncoder`] callbacks.
369#[derive(Clone, Copy, Debug, PartialEq)]
370pub struct StreamCtx<'a> {
371    pub kind: ColumnKind,
372    pub stream_type: StreamType,
373    pub name: &'a str,
374    pub subname: &'a str,
375}
376impl<'a> StreamCtx<'a> {
377    /// Stream with a logical sub-part (e.g. string column `"lengths"` / `"offsets"`, shared-dict child suffix).
378    #[inline]
379    #[must_use]
380    pub const fn new(
381        kind: ColumnKind,
382        stream_type: StreamType,
383        name: &'a str,
384        subname: &'a str,
385    ) -> Self {
386        Self {
387            kind,
388            stream_type,
389            name,
390            subname,
391        }
392    }
393
394    #[inline]
395    #[must_use]
396    pub const fn id(stream_type: StreamType) -> Self {
397        Self::new(ColumnKind::Id, stream_type, "", "")
398    }
399
400    #[inline]
401    #[must_use]
402    pub const fn geom(stream_type: StreamType, name: &'a str) -> Self {
403        Self::new(ColumnKind::Geometry, stream_type, name, "")
404    }
405
406    #[inline]
407    #[must_use]
408    pub const fn prop(stream_type: StreamType, name: &'a str) -> Self {
409        Self::new(ColumnKind::Property, stream_type, name, "")
410    }
411
412    #[inline]
413    #[must_use]
414    pub const fn prop_data(name: &'a str) -> Self {
415        let stream_type = StreamType::Data(DictionaryType::None);
416        Self::new(ColumnKind::Property, stream_type, name, "")
417    }
418
419    #[inline]
420    #[must_use]
421    pub const fn prop2(stream_type: StreamType, prefix: &'a str, suffix: &'a str) -> Self {
422        Self::new(ColumnKind::Property, stream_type, prefix, suffix)
423    }
424}
425
426/// Explicit, deterministic encoding configuration for synthetics and tests.
427///
428/// All encoding choices are caller-specified via callbacks so one struct can cover
429/// any combination without per-stream boilerplate.
430///
431/// Always compiled; publicly visible only when the `__private` feature is enabled
432/// (re-exported from [`crate::encoder`]).
433#[derive(Dbg)]
434pub struct ExplicitEncoder {
435    /// Vertex buffer layout for geometry streams.
436    pub vertex_buffer_type: VertexBufferType,
437    /// Per-stream override for the skip-empty-stream rule used by `write_geo_u32_stream`.
438    #[dbg(skip)]
439    pub force_stream: Box<dyn for<'a> Fn(&'a StreamCtx<'a>) -> bool>,
440    /// Return the [`IntEncoder`] for a stream identified by [`StreamCtx`].
441    #[dbg(skip)]
442    pub get_int_encoder: Box<dyn for<'a> Fn(&'a StreamCtx<'a>) -> IntEncoder>,
443    /// Return the string encoding strategy for a string property column.
444    #[dbg(skip)]
445    pub get_str_encoding: Box<dyn Fn(&str) -> StrEncoding>,
446}