Skip to main content

mlt_core/encoder/geometry/
encode.rs

1use std::collections::HashMap;
2use std::mem;
3
4use geo_types::Coord;
5use probabilistic_collections::SipHasherBuilder;
6use probabilistic_collections::hyperloglog::HyperLogLog;
7use usize_cast::{FromUsize as _, IntoUsize as _};
8
9use super::model::VertexBufferType;
10use crate::MltResult;
11use crate::codecs::hilbert::hilbert_sort_key;
12use crate::codecs::zigzag::encode_componentwise_delta_vec2s;
13use crate::decoder::GeometryType::{LineString, Point, Polygon};
14use crate::decoder::{
15    ColumnType, DictionaryType, GeometryType, GeometryValues, LengthType, LogicalEncoding, Morton,
16    OffsetType, PhysicalEncoding, StreamMeta, StreamType,
17};
18use crate::encoder::model::{CurveParams, StreamCtx};
19use crate::encoder::{Codecs, Encoder, PhysicalCodecs, write_stream_payload};
20
21/// Compute `ZOrderCurve` parameters from the vertex value range.
22///
23/// Returns `(bits, shift)` matching Java's `SpaceFillingCurve`.
24/// Build a sorted unique Morton dictionary and per-vertex offset indices from a flat
25/// `[x0, y0, x1, y1, …]` vertex slice.
26///
27/// Returns `(sorted_unique_codes, per_vertex_offsets)`.
28#[hotpath::measure]
29fn build_morton_dict(vertices: &[i32], meta: Morton) -> MltResult<(Vec<u32>, Vec<u32>)> {
30    let codes: Vec<u32> = vertices
31        .as_chunks::<2>()
32        .0
33        .iter()
34        .map(|&[x, y]| meta.encode_morton(x, y))
35        .collect::<Result<_, _>>()?;
36
37    let mut dict = codes.clone();
38    dict.sort_unstable();
39    dict.dedup();
40
41    #[expect(
42        clippy::cast_possible_truncation,
43        reason = "dict.len() <= u32::MAX (deduped u32 codes)"
44    )]
45    let code_to_idx: HashMap<u32, u32> = dict
46        .iter()
47        .enumerate()
48        .map(|(i, &c)| (c, i as u32))
49        .collect();
50    let offsets: Vec<u32> = codes.iter().map(|code| code_to_idx[code]).collect();
51
52    Ok((dict, offsets))
53}
54
55/// Build a Hilbert-curve-sorted unique vertex dictionary into caller-provided
56/// scratch.
57///
58/// On return, `dict_xy` holds the deduplicated `[x, y, …]` dictionary in
59/// Hilbert order and `offsets[i]` is the slot of input vertex `i`; `indexed`
60/// and `remap` are left as opaque scratch.
61///
62/// Dedup is keyed on the Hilbert curve index. Inside the `params.bits` grid
63/// the index <-> `(x, y)` mapping is bijective, so dedup-by-index is equivalent
64/// to dedup-by-coordinate without the cost of hashing pairs.
65#[hotpath::measure]
66fn build_hilbert_dict(
67    vertices: &[i32],
68    params: CurveParams,
69    offsets: &mut Vec<u32>,
70    indexed: &mut Vec<u64>,
71    dict_xy: &mut Vec<i32>,
72    remap: &mut HashMap<u32, u32>,
73) {
74    offsets.clear();
75    indexed.clear();
76    dict_xy.clear();
77    remap.clear();
78
79    let coord_count = vertices.len() / 2;
80    if coord_count == 0 {
81        return;
82    }
83    offsets.reserve(coord_count);
84    indexed.reserve(coord_count);
85    dict_xy.reserve(coord_count * 2);
86    remap.reserve(coord_count);
87
88    for (i, &[x, y]) in vertices.as_chunks::<2>().0.iter().enumerate() {
89        let k = hilbert_sort_key(Coord { x, y }, params);
90        offsets.push(k);
91        // Key in the high 32 bits so a single u64 sort orders by Hilbert
92        // index while preserving the original position for tie-breaking.
93        let packed = (u64::from(k) << 32) | u64::from_usize(i);
94        indexed.push(packed);
95    }
96    indexed.sort_unstable();
97
98    let mut last_key: Option<u32> = None;
99    for &packed in &*indexed {
100        let key = (packed >> 32) as u32;
101        let src_idx = ((packed & 0xFFFF_FFFF) as u32).into_usize();
102        if last_key != Some(key) {
103            #[expect(
104                clippy::cast_possible_truncation,
105                reason = "dict.len() <= coord_count <= u32::MAX"
106            )]
107            let slot = (dict_xy.len() / 2) as u32;
108            dict_xy.push(vertices[src_idx * 2]);
109            dict_xy.push(vertices[src_idx * 2 + 1]);
110            remap.insert(key, slot);
111            last_key = Some(key);
112        }
113    }
114
115    for k in offsets.iter_mut() {
116        *k = remap[k];
117    }
118}
119
120/// Push consecutive offset-differences from `offsets` onto `lengths`.
121///
122/// Expects a slice of `n + 1` elements and produces `n` lengths,
123/// one per consecutive pair: `offsets[i + 1] - offsets[i]`.
124#[inline]
125fn extend_offsets(lengths: &mut Vec<u32>, offsets: &[u32]) -> usize {
126    lengths.extend(offsets.windows(2).map(|w| w[1] - w[0]));
127    offsets.len() - 1
128}
129
130/// Convert geometry offsets to length stream for encoding.
131/// This is the inverse of `decode_root_length_stream`.
132///
133/// The offset array can be either:
134/// - Sparse: entries only for geometries that need them (types > `buffer_id`), N+1 entries for N matching geoms
135/// - Dense (normalized): N+1 entries for N geometry types, indexed by geometry position
136///
137/// If dense `(len == geom_types.len() + 1)`, use geometry index directly.
138/// If sparse, use sequential indexing for matching geometry types.
139fn encode_root_length_stream(
140    geom_types: &[GeometryType],
141    geom_offsets: &[u32],
142    buffer_id: GeometryType,
143) -> Vec<u32> {
144    if geom_offsets.len() == geom_types.len() + 1 {
145        // Dense: zip by position, then filter out non-contributing types.
146        geom_types
147            .iter()
148            .zip(geom_offsets.windows(2))
149            .filter(|&(&t, _)| t > buffer_id)
150            .map(|(_, w)| w[1] - w[0])
151            .collect()
152    } else {
153        // Sparse: filter types first, then zip with consecutive offset pairs.
154        geom_types
155            .iter()
156            .filter(|&&t| t > buffer_id)
157            .zip(geom_offsets.windows(2))
158            .map(|(_, w)| w[1] - w[0])
159            .collect()
160    }
161}
162
163/// Convert part offsets to length stream for level 1 encoding.
164fn encode_level1_length_stream(
165    geom_types: &[GeometryType],
166    geom_offsets: &[u32],
167    part_offsets: &[u32],
168    is_line_string_present: bool,
169) -> Vec<u32> {
170    let mut lengths = Vec::new();
171    let mut part_idx = 0;
172
173    for (i, &geom_type) in geom_types.iter().enumerate() {
174        if geom_type.is_polygon() || (is_line_string_present && geom_type.is_linestring()) {
175            let n = (geom_offsets[i + 1] - geom_offsets[i]).into_usize();
176            part_idx += extend_offsets(&mut lengths, &part_offsets[part_idx..=part_idx + n]);
177        }
178        // Note: Point/MultiPoint don't have entries in the sparse part_offsets used
179        // at this call site, so part_idx must not advance for non-length types here.
180    }
181
182    lengths
183}
184
185/// Compute ring vertex-count lengths for the no-geometry-offsets + has-ring-offsets case.
186///
187/// In this branch `part_offsets` is a **dense** N+1 array (one slot per geometry,
188/// including Points) and `ring_offsets` holds the vertex offsets for every slot.
189/// Using the geometry index directly as the ring-slot index avoids the
190/// running-counter misalignment that `encode_level1_length_stream` would produce
191/// when non-length types (Points) occupy slots that a sparse counter skips.
192fn encode_ring_lengths_for_mixed(
193    geom_types: &[GeometryType],
194    part_offsets: &[u32],
195    ring_offsets: &[u32],
196    has_line_string: bool,
197) -> Vec<u32> {
198    let mut lengths = Vec::new();
199    for (i, &geom_type) in geom_types.iter().enumerate() {
200        if geom_type.is_polygon() || (has_line_string && geom_type.is_linestring()) {
201            let s = part_offsets[i].into_usize();
202            let e = part_offsets[i + 1].into_usize();
203            extend_offsets(&mut lengths, &ring_offsets[s..=e]);
204        }
205    }
206    lengths
207}
208
209/// Convert ring offsets to length stream for level 2 encoding.
210/// This is the inverse of `decode_level2_length_stream`.
211///
212/// The `geom_offsets` array is expected to be an N+1 element array for N geometries.
213/// The `part_offsets` array tracks ring counts cumulatively.
214fn encode_level2_length_stream(
215    geom_types: &[GeometryType],
216    geom_offsets: &[u32],
217    part_offsets: &[u32],
218    ring_offsets: &[u32],
219) -> Vec<u32> {
220    let mut lengths = Vec::new();
221    let mut part_idx = 0;
222    let mut ring_idx = 0;
223
224    for (i, &geom_type) in geom_types.iter().enumerate() {
225        let count = (geom_offsets[i + 1] - geom_offsets[i]).into_usize();
226
227        // Only Polygon and MultiPolygon have ring data in level 2
228        // LineStrings with Polygon present add their vertex counts directly to ring_offsets,
229        // but they don't have parts (ring count per linestring is always 1 implicitly)
230        if geom_type.is_polygon() {
231            // Polygon/MultiPolygon: iterate through sub-polygons, each has parts (ring counts)
232            for _ in 0..count {
233                let n = (part_offsets[part_idx + 1] - part_offsets[part_idx]).into_usize();
234                ring_idx += extend_offsets(&mut lengths, &ring_offsets[ring_idx..=ring_idx + n]);
235                part_idx += 1;
236            }
237        } else if geom_type.is_linestring() {
238            // LineStrings contribute to ring_offsets directly (vertex counts)
239            ring_idx += extend_offsets(&mut lengths, &ring_offsets[ring_idx..=ring_idx + count]);
240        }
241        // Note: Point/MultiPoint don't contribute to ring_offsets
242    }
243
244    lengths
245}
246
247/// Convert part offsets without ring buffer to length stream.
248///
249/// This path is reached only when `ring_offsets` is absent, which means no Polygon/MultiPolygon
250/// types are present (they always create `ring_offsets`).  Only LineString/MultiLineString
251/// contribute vertex-count lengths here; Point/MultiPoint use an implicit count of 1 in the
252/// decoder and produce no entry in this stream.
253fn encode_level1_without_ring_buffer_length_stream(
254    geom_types: &[GeometryType],
255    geom_offsets: &[u32],
256    part_offsets: &[u32],
257) -> Vec<u32> {
258    let mut lengths = Vec::new();
259    let mut part_idx = 0;
260
261    for (i, &geom_type) in geom_types.iter().enumerate() {
262        if geom_type.is_linestring() {
263            let n = (geom_offsets[i + 1] - geom_offsets[i]).into_usize();
264            part_idx += extend_offsets(&mut lengths, &part_offsets[part_idx..=part_idx + n]);
265        }
266        // Point/MultiPoint don't contribute to part_offsets; part_idx must not advance.
267    }
268
269    lengths
270}
271
272/// Normalize `geom_offsets` for mixed geometry types.
273fn normalize_geometry_offsets(vector_types: &[GeometryType], geom_offsets: &[u32]) -> Vec<u32> {
274    let mut normalized = Vec::with_capacity(vector_types.len() + 1);
275    let mut offset = 0_u32;
276    let mut sparse_idx = 0_usize; // Index into sparse geom_offsets
277
278    for &geom_type in vector_types {
279        normalized.push(offset);
280
281        if geom_type.is_multi() {
282            // Multi* types get their count from the sparse array
283            if sparse_idx + 1 < geom_offsets.len() {
284                let start = geom_offsets[sparse_idx];
285                let end = geom_offsets[sparse_idx + 1];
286                offset += end - start;
287                sparse_idx += 1;
288            }
289        } else {
290            // Non-Multi types have implicit count of 1
291            offset += 1;
292        }
293    }
294
295    normalized.push(offset);
296    normalized
297}
298
299/// Normalize `part_offsets` for ring-based indexing (Polygon mixed with `Point`/`LineString`).
300///
301/// Called only when `geom_offsets` is absent (no Multi\* types) and `ring_offsets` is
302/// present.  In this context `part_offsets` is a compact polygon-only array; this function
303/// expands it to a dense per-geometry array so that `encode_ring_lengths_for_mixed` can index
304/// directly by geometry position.
305///
306/// Each slot in the output holds the first index into `ring_offsets` for that geometry:
307/// - `Point`: no contribution - slot range is empty (`ring_idx` unchanged).
308/// - `LineString`: contributes 1 slot (vertex count) - slot range is 1.
309/// - `Polygon`: contributes `ring_count` slots - slot range equals its ring count.
310fn normalize_part_offsets_for_rings(
311    vector_types: &[GeometryType],
312    part_offsets: &[u32],
313    ring_offsets: &[u32],
314) -> Vec<u32> {
315    let mut normalized = Vec::with_capacity(vector_types.len() + 1);
316    let mut ring_idx = 0_u32;
317    let mut part_idx = 0_usize;
318
319    for &geom_type in vector_types {
320        normalized.push(ring_idx);
321
322        if geom_type == Point {
323            // Point has no vertex-count slot in ring_offsets.
324        } else if geom_type.is_linestring() {
325            // Each LineString occupies exactly one slot in ring_offsets.
326            ring_idx += 1;
327        } else if geom_type.is_polygon() && part_idx + 1 < part_offsets.len() {
328            // Polygon occupies ring_count slots (one vertex-count per ring).
329            let ring_count = part_offsets[part_idx + 1] - part_offsets[part_idx];
330            ring_idx += ring_count;
331            part_idx += 1;
332        }
333        // No Multi* types can appear here (they always produce geom_offsets).
334    }
335
336    // ring_idx must equal ring_offsets.len() - 1 for well-formed data.
337    debug_assert_eq!(
338        ring_idx.into_usize(),
339        ring_offsets.len().saturating_sub(1),
340        "ring index mismatch after normalization"
341    );
342    normalized.push(ring_idx);
343    normalized
344}
345
346/// Whether to race dictionary-based vertex layouts (Hilbert, Morton) against
347/// the plain Vec2 layout for this geometry column.
348///
349/// Profiling showed unconditional racing is ~2× slower overall: most layers
350/// have high vertex uniqueness, where the dict layouts cannot win and the
351/// extra sort + `HashMap` build is wasted. Gate on Morton fitting in 16 bits
352/// per axis (required by the spec) and on a `HyperLogLog`-estimated
353/// uniqueness ratio below the threshold.
354#[hotpath::measure]
355fn dict_may_be_beneficial(vertices: &[i32], enc: &Encoder) -> bool {
356    const MAXIMUM_UNIQUENESS_THRESHOLD_FOR_DICT: f64 = 0.66;
357
358    let coord_count = vertices.len() / 2;
359    if coord_count == 0 || enc.morton_cache.is_none() {
360        return false;
361    }
362
363    let mut hll = HyperLogLog::<Coord<i32>>::with_hasher(0.03, SipHasherBuilder::from_seed(0, 0));
364    for &[x, y] in vertices.as_chunks::<2>().0 {
365        hll.insert(&Coord::<i32> { x, y });
366    }
367    #[expect(clippy::cast_precision_loss)]
368    let estimated_unique = hll.len().clamp(0.0, coord_count as f64);
369    #[expect(clippy::cast_precision_loss)]
370    let uniqueness_ratio = estimated_unique / coord_count as f64;
371    uniqueness_ratio < MAXIMUM_UNIQUENESS_THRESHOLD_FOR_DICT
372}
373
374/// Pre-populated by [`StagedLayer::encode_into`](crate::encoder::StagedLayer::encode_into);
375/// callers must have gated on [`dict_may_be_beneficial`] which rejects layers
376/// whose extent does not fit Morton.
377fn get_morton(enc: &Encoder) -> Morton {
378    enc.morton_cache.expect(
379        "morton_cache populated by StagedLayer::encode_into; gated by dict_may_be_beneficial",
380    )
381}
382
383/// Pre-populated by [`StagedLayer::encode_into`](crate::encoder::StagedLayer::encode_into).
384fn get_hilbert_params(enc: &Encoder) -> CurveParams {
385    enc.hilbert_cache
386        .expect("hilbert_cache populated by StagedLayer::encode_into")
387}
388
389/// Encode the plain Vec2 vertex layout: componentwise-delta over the raw
390/// `[x0, y0, x1, y1, …]` slice.
391fn encode_vec2_vertex_stream(
392    vertices: &[i32],
393    enc: &mut Encoder,
394    codecs: &mut Codecs,
395) -> MltResult<u8> {
396    let delta = encode_componentwise_delta_vec2s(vertices, &mut codecs.logical.u32_tmp);
397    let ctx = StreamCtx::geom(StreamType::Data(DictionaryType::Vertex), "vertex");
398    let logical = LogicalEncoding::ComponentwiseDelta;
399    write_geo_precomputed_stream(delta, ctx, logical, enc, &mut codecs.physical)
400}
401
402/// Encode a Morton-keyed vertex dictionary: per-vertex offsets stream
403/// followed by a delta-encoded Morton-code dictionary.
404fn encode_morton_vertex_streams(
405    vertices: &[i32],
406    enc: &mut Encoder,
407    codecs: &mut Codecs,
408) -> MltResult<u8> {
409    let morton = get_morton(enc);
410    let (dict, offsets) = build_morton_dict(vertices, morton)?;
411    let mut n: u8 = 0;
412
413    let ctx = StreamCtx::geom(StreamType::Offset(OffsetType::Vertex), "vertex_offsets");
414    n += write_geo_u32_stream(&offsets, ctx, enc, codecs)?;
415
416    let delta = encode_morton_deltas(&dict, &mut codecs.logical.u32_tmp);
417    let ctx = StreamCtx::geom(StreamType::Data(DictionaryType::Morton), "vertex");
418    let logical = LogicalEncoding::MortonDelta(morton);
419    n += write_geo_precomputed_stream(delta, ctx, logical, enc, &mut codecs.physical)?;
420    Ok(n)
421}
422
423/// Encode a Hilbert-keyed vertex dictionary: per-vertex offsets stream
424/// followed by a componentwise-delta-encoded `[x, y, …]` dictionary in
425/// Hilbert order.
426fn encode_hilbert_vertex_streams(
427    vertices: &[i32],
428    enc: &mut Encoder,
429    codecs: &mut Codecs,
430) -> MltResult<u8> {
431    let params = get_hilbert_params(enc);
432    let mut n: u8 = 0;
433
434    // Take scratch ownership locally: `write_geo_*_stream` needs `&mut Codecs`,
435    // which would otherwise conflict with our `&[..]` views into these slots.
436    let mut offsets = mem::take(&mut codecs.logical.hilbert_offsets);
437    let mut indexed = mem::take(&mut codecs.logical.hilbert_indexed);
438    let mut dict_xy = mem::take(&mut codecs.logical.hilbert_dict_xy);
439    let mut remap = mem::take(&mut codecs.logical.hilbert_remap);
440
441    build_hilbert_dict(
442        vertices,
443        params,
444        &mut offsets,
445        &mut indexed,
446        &mut dict_xy,
447        &mut remap,
448    );
449    // Done with these - restore so the physical-encoding race below can use
450    // them via the codec.
451    codecs.logical.hilbert_indexed = indexed;
452    codecs.logical.hilbert_remap = remap;
453
454    let ctx = StreamCtx::geom(StreamType::Offset(OffsetType::Vertex), "vertex_offsets");
455    n += write_geo_u32_stream(&offsets, ctx, enc, codecs)?;
456
457    // Reuse `offsets` as the delta output rather than allocating another Vec;
458    // also keeps `codecs.logical.u32_values` free for the inner race.
459    encode_componentwise_delta_vec2s(&dict_xy, &mut offsets);
460    let ctx = StreamCtx::geom(StreamType::Data(DictionaryType::Vertex), "vertex");
461    let logical = LogicalEncoding::ComponentwiseDelta;
462    n += write_geo_precomputed_stream(&offsets, ctx, logical, enc, &mut codecs.physical)?;
463
464    codecs.logical.hilbert_offsets = offsets;
465    codecs.logical.hilbert_dict_xy = dict_xy;
466    Ok(n)
467}
468
469/// Write a geometry `u32` stream: [`Encoder::override_int_enc`] when explicit mode is active,
470/// otherwise try all pruned candidates and keep the shortest.
471///
472/// Returns `1` if the stream was written, `0` if it was skipped.  Empty streams are skipped
473/// unless [`Encoder::force_stream`] returns `true` for this stream's [`StreamCtx`].
474fn write_geo_u32_stream(
475    data: &[u32],
476    ctx: StreamCtx,
477    enc: &mut Encoder,
478    codecs: &mut Codecs,
479) -> MltResult<u8> {
480    Ok(if data.is_empty() && !enc.force_stream(&ctx) {
481        0
482    } else {
483        codecs.write_int_stream(data, &ctx, enc)?;
484        1
485    })
486}
487
488/// Like [`write_geo_u32_stream`] but for pre-logically-encoded data: competes
489/// only the physical encoders instead of applying a logical transform.
490///
491/// Returns `1` if the stream was written, `0` if skipped (empty + no force).
492fn write_geo_precomputed_stream(
493    data: &[u32],
494    ctx: StreamCtx,
495    logical: LogicalEncoding,
496    enc: &mut Encoder,
497    physical: &mut PhysicalCodecs,
498) -> MltResult<u8> {
499    use PhysicalEncoding as PE;
500
501    Ok(if data.is_empty() && !enc.force_stream(&ctx) {
502        0
503    } else {
504        if let Some(int_enc) = enc.override_int_enc(&ctx) {
505            physical.write_encoded_as::<[u32]>(&ctx, enc, logical, data, int_enc.physical)?;
506        } else if data.is_empty() {
507            let meta = StreamMeta::new2(ctx.stream_type, logical, PE::None, 0)?;
508            write_stream_payload(enc, meta, false, &[])?;
509        } else {
510            let allow_fastpfor = enc.config().allow_fastpfor();
511            let mut alt = enc.try_alternatives();
512            if allow_fastpfor {
513                alt.with(|enc| {
514                    let vals = physical.fastpfor(data)?;
515                    let meta =
516                        StreamMeta::new2(ctx.stream_type, logical, PE::FastPFor256, data.len())?;
517                    write_stream_payload(enc, meta, false, vals)
518                })?;
519            }
520            alt.with(|enc| {
521                let vals = physical.varint(data);
522                let meta = StreamMeta::new2(ctx.stream_type, logical, PE::VarInt, data.len())?;
523                write_stream_payload(enc, meta, false, vals)
524            })?;
525        }
526        1
527    })
528}
529
530impl GeometryValues {
531    /// Write the geometry column to `enc`.
532    #[hotpath::measure]
533    pub fn write_to(self, enc: &mut Encoder, codecs: &mut Codecs) -> MltResult<()> {
534        let Self {
535            vector_types,
536            geometry_offsets,
537            part_offsets,
538            ring_offsets,
539            index_buffer,
540            triangles,
541            vertices,
542        } = self;
543
544        // Flatten every Option<Vec> -> Vec  (empty == not present).
545        // triangles: None means no tessellation; Some([]) can't occur in practice (each
546        // push_geom appends a count), so empty == absent is safe here too.
547        // vertices: None means no coordinate data (e.g. empty layer).
548        let geom_offsets = geometry_offsets.unwrap_or_default();
549        let part_offsets = part_offsets.unwrap_or_default();
550        let ring_offsets = ring_offsets.unwrap_or_default();
551        let index_buffer = index_buffer.unwrap_or_default();
552        let triangles = triangles.unwrap_or_default();
553        let vertices = vertices.unwrap_or_default();
554
555        // Direct callers (tests, custom drivers) skip `StagedLayer::encode_into`
556        // and arrive with empty caches; populate from `vertices` so the
557        // dictionary builders can rely on them unconditionally.
558        if enc.hilbert_cache.is_none() {
559            enc.hilbert_cache = Some(CurveParams::from_vertices(&vertices));
560        }
561        if enc.morton_cache.is_none() {
562            let p = enc.hilbert_cache.expect("populated above");
563            enc.morton_cache = Morton::new(p.bits, p.shift).ok();
564        }
565
566        let meta: Vec<u32> = vector_types.iter().map(|t| *t as u32).collect();
567
568        let part_offsets = if geom_offsets.is_empty()
569            && !ring_offsets.is_empty()
570            && !part_offsets.is_empty()
571            && part_offsets.len() != vector_types.len() + 1
572        {
573            // Normalize part_offsets when there are no geometry offsets but ring offsets exist.
574            normalize_part_offsets_for_rings(&vector_types, &part_offsets, &ring_offsets)
575        } else {
576            part_offsets
577        };
578
579        // Write column type to meta; reserve exactly 1 byte for stream count
580        // (geometry never exceeds ~8 streams, always fits in a single varint byte).
581        enc.write_column_type(ColumnType::Geometry)?;
582        let stream_count_pos = enc.data().len();
583        enc.data_mut().push(0); // placeholder - patched below
584        let mut n: u8 = 0;
585
586        // Meta stream - always written, even for a zero-feature layer.
587        let ctx = StreamCtx::geom(StreamType::Length(LengthType::VarBinary), "meta");
588        codecs.write_int_stream(&meta, &ctx, enc)?;
589        n += 1;
590
591        // Topology: compute each length stream and write it immediately.
592        if !geom_offsets.is_empty() {
593            let geom_offsets = if geom_offsets.len() == vector_types.len() + 1 {
594                geom_offsets
595            } else {
596                normalize_geometry_offsets(&vector_types, &geom_offsets)
597            };
598            let data = encode_root_length_stream(&vector_types, &geom_offsets, Polygon);
599            let ctx = StreamCtx::geom(StreamType::Length(LengthType::Geometries), "geometries");
600            n += write_geo_u32_stream(&data, ctx, enc, codecs)?;
601
602            // part_offsets is intentionally kept sparse here (polygon-only cumulative
603            // ring counts). encode_level1/2_length_stream navigate it with a running
604            // part_idx counter that advances only for Polygon/LineString types, which
605            // matches the sparse layout. Densifying via normalize_part_offsets_for_rings
606            // would insert Point slots and corrupt the counter arithmetic.
607            if !part_offsets.is_empty() {
608                if ring_offsets.is_empty() {
609                    // geom -> parts only (no rings).
610                    let data = encode_level1_without_ring_buffer_length_stream(
611                        &vector_types,
612                        &geom_offsets,
613                        &part_offsets,
614                    );
615                    let ctx = StreamCtx::geom(StreamType::Length(LengthType::Parts), "no_rings");
616                    n += write_geo_u32_stream(&data, ctx, enc, codecs)?;
617                } else {
618                    // Full topology: geom -> parts -> rings.
619                    // LineStrings contribute to rings here, not to parts.
620                    let data = encode_level1_length_stream(
621                        &vector_types,
622                        &geom_offsets,
623                        &part_offsets,
624                        false,
625                    );
626                    let ctx = StreamCtx::geom(StreamType::Length(LengthType::Parts), "rings");
627                    n += write_geo_u32_stream(&data, ctx, enc, codecs)?;
628
629                    let data = encode_level2_length_stream(
630                        &vector_types,
631                        &geom_offsets,
632                        &part_offsets,
633                        &ring_offsets,
634                    );
635                    let ctx = StreamCtx::geom(StreamType::Length(LengthType::Rings), "rings2");
636                    n += write_geo_u32_stream(&data, ctx, enc, codecs)?;
637                }
638            }
639        } else if !part_offsets.is_empty() {
640            if ring_offsets.is_empty() {
641                let data = encode_root_length_stream(&vector_types, &part_offsets, Point);
642                let ctx = StreamCtx::geom(StreamType::Length(LengthType::Parts), "no_rings");
643                n += write_geo_u32_stream(&data, ctx, enc, codecs)?;
644            } else {
645                // No Multi* types; parts -> rings (Polygon / mixed Point+Polygon).
646                // Java writes an empty GEOMETRIES stream here for tessellated polygons; only do
647                // so when explicitly forced (e.g. to preserve byte-for-byte Java compatibility).
648                let ctx = StreamCtx::geom(StreamType::Length(LengthType::Geometries), "geometries");
649                n += write_geo_u32_stream(&[], ctx, enc, codecs)?;
650
651                let data = encode_root_length_stream(&vector_types, &part_offsets, LineString);
652                let ctx = StreamCtx::geom(StreamType::Length(LengthType::Parts), "parts");
653                n += write_geo_u32_stream(&data, ctx, enc, codecs)?;
654
655                // part_offs is a dense N+1 array (one slot per geometry incl. Points);
656                // ring_offs stores vertex offsets per slot.  The dense-aware helper skips
657                // Point slots by index rather than a running counter.
658                let has_line_string = vector_types
659                    .iter()
660                    .copied()
661                    .any(GeometryType::is_linestring);
662                let data = encode_ring_lengths_for_mixed(
663                    &vector_types,
664                    &part_offsets,
665                    &ring_offsets,
666                    has_line_string,
667                );
668                let ctx = StreamCtx::geom(StreamType::Length(LengthType::Rings), "parts_ring");
669                n += write_geo_u32_stream(&data, ctx, enc, codecs)?;
670            }
671        }
672
673        let ctx = StreamCtx::geom(StreamType::Length(LengthType::Triangles), "triangles");
674        n += write_geo_u32_stream(&triangles, ctx, enc, codecs)?;
675        let ctx = StreamCtx::geom(StreamType::Offset(OffsetType::Index), "triangles_indexes");
676        n += write_geo_u32_stream(&index_buffer, ctx, enc, codecs)?;
677
678        if let Some(forced) = enc.override_vertex_buffer_type() {
679            n += match forced {
680                VertexBufferType::Vec2 => encode_vec2_vertex_stream(&vertices, enc, codecs)?,
681                VertexBufferType::Morton => encode_morton_vertex_streams(&vertices, enc, codecs)?,
682                VertexBufferType::Hilbert => encode_hilbert_vertex_streams(&vertices, enc, codecs)?,
683            };
684        } else if dict_may_be_beneficial(&vertices, enc) {
685            // Morton fits (the gate above ensures it), so race all three.
686            let mut winner_size: usize = usize::MAX;
687            let mut winner_stream_cnt: u8 = 0;
688            let mut alt = enc.try_alternatives();
689            alt.with(|e| {
690                let ds = e.data().len();
691                let ms = e.meta().len();
692                winner_stream_cnt = encode_vec2_vertex_stream(&vertices, e, codecs)?;
693                winner_size = (e.data().len() - ds) + (e.meta().len() - ms);
694                Ok(())
695            })?;
696            alt.with(|e| {
697                let ds = e.data().len();
698                let ms = e.meta().len();
699                let cnt = encode_hilbert_vertex_streams(&vertices, e, codecs)?;
700                let size = (e.data().len() - ds) + (e.meta().len() - ms);
701                if size < winner_size {
702                    winner_stream_cnt = cnt;
703                    winner_size = size;
704                }
705                Ok(())
706            })?;
707            alt.with(|e| {
708                let ds = e.data().len();
709                let ms = e.meta().len();
710                let cnt = encode_morton_vertex_streams(&vertices, e, codecs)?;
711                let size = (e.data().len() - ds) + (e.meta().len() - ms);
712                if size < winner_size {
713                    winner_stream_cnt = cnt;
714                }
715                Ok(())
716            })?;
717            drop(alt);
718            n += winner_stream_cnt;
719        } else {
720            n += encode_vec2_vertex_stream(&vertices, enc, codecs)?;
721        }
722
723        // Patch the reserved stream-count byte.
724        debug_assert!(n <= 127, "geometry stream count must fit in one byte");
725        enc.data_mut()[stream_count_pos] = n;
726        Ok(())
727    }
728}
729
730fn encode_morton_deltas<'a>(codes: &[u32], buffer: &'a mut Vec<u32>) -> &'a mut Vec<u32> {
731    buffer.clear();
732    if let Some(&first) = codes.first() {
733        buffer.reserve(codes.len());
734        buffer.extend(std::iter::once(first).chain(codes.windows(2).map(|w| w[1] - w[0])));
735    }
736    buffer
737}
738
739#[cfg(test)]
740mod tests {
741    use super::*;
742
743    #[test]
744    fn test_build_morton_dict() {
745        let meta = Morton { bits: 4, shift: 0 };
746        // vertices: [x0,y0, x1,y1, x2,y2, x3,y3] - repeat (1,2) to test dedup
747        let vertices = [1, 2, 3, 4, 1, 2, 0, 0];
748        let (dict, offsets) = build_morton_dict(&vertices, meta).unwrap();
749
750        assert!(
751            dict.windows(2).all(|w| w[0] < w[1]),
752            "dict not sorted/unique"
753        );
754        assert_eq!(offsets.len(), 4, "offsets length == number of vertex pairs");
755        assert_eq!(offsets[0], offsets[2], "duplicate (1,2) should share index");
756        assert!(offsets.iter().all(|&o| o.into_usize() < dict.len()));
757    }
758
759    #[test]
760    fn test_encode_root_length_stream() {
761        // Single Polygon geometry (no Multi)
762        let types = vec![Polygon];
763        let offsets = vec![0, 1]; // One polygon
764
765        let lengths = encode_root_length_stream(&types, &offsets, Polygon);
766        // Polygon == buffer_id, so no length encoded
767        assert_eq!(lengths, [] as [u32; 0]);
768
769        // MultiPolygon needs length encoded
770        let types = vec![GeometryType::MultiPolygon];
771        let offsets = vec![0, 2]; // MultiPolygon with 2 polygons
772
773        let lengths = encode_root_length_stream(&types, &offsets, Polygon);
774        assert_eq!(lengths, vec![2]);
775    }
776}