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