1use crate::error::{Error, Result};
81use crate::types::GeometryType;
82use arrow::array::{
83 Array, ArrayRef, DictionaryArray, FixedSizeListArray, Float32Array, Float32Builder,
84 Float64Array, Int32Array, Int32Builder, Int64Array, Int64Builder, ListArray, ListBuilder,
85 RecordBatch, StringArray, UInt16Array, UInt16Builder, UInt32Builder, UInt64Array, UInt8Array,
86};
87use arrow::buffer::OffsetBuffer;
88use arrow::datatypes::{DataType, Field, Schema, UInt16Type};
89use arrow::ipc::reader::StreamReader;
90use arrow::ipc::writer::StreamWriter;
91use arrow::ipc::{root_as_message, MessageHeader};
92use serde::{Deserialize, Serialize};
93use std::borrow::Cow;
94use std::collections::{BTreeMap, HashMap, HashSet};
95use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
96use std::sync::{Arc, Mutex, OnceLock, RwLock};
97
98pub const ALIGNED_FRAME_FLAG: u16 = 0x8000;
103
104const FRAME_ALIGN: usize = 8;
106
107pub const FRAME_V2_ESCAPE: u16 = 0xFFFF;
117
118const FRAME_V2_VERSION: u8 = 2;
120
121pub const SECTION_INLINE_SCHEMA_CORE: u8 = 0x01;
124pub const SECTION_TILE_META: u8 = 0x02;
126pub const SECTION_CORE_BATCH: u8 = 0x03;
128pub const SECTION_INLINE_SCHEMA_PROPS: u8 = 0x04;
130pub const SECTION_PROPS_BATCH: u8 = 0x05;
132
133const REF_KIND_INLINE: u8 = 0;
136const REF_KIND_TEMPLATE_HASH: u8 = 1;
139const REF_KIND_NO_PROPS: u8 = 2;
142
143pub const FORMAT_VERSION_V1: u32 = 1;
148pub const FORMAT_VERSION_V2: u32 = 2;
150
151pub(crate) fn blake3_128(bytes: &[u8]) -> [u8; 16] {
154 let hash = blake3::hash(bytes);
155 let mut out = [0u8; 16];
156 out.copy_from_slice(&hash.as_bytes()[..16]);
157 out
158}
159
160#[derive(Debug, Default)]
169pub struct TemplateCollector {
170 templates: Mutex<BTreeMap<[u8; 16], Vec<u8>>>,
171}
172
173impl TemplateCollector {
174 pub fn new() -> Self {
176 Self::default()
177 }
178
179 pub fn record(&self, template: &[u8]) -> [u8; 16] {
182 let hash = blake3_128(template);
183 self.templates
184 .lock()
185 .unwrap()
186 .entry(hash)
187 .or_insert_with(|| template.to_vec());
188 hash
189 }
190
191 pub fn snapshot(&self) -> Vec<([u8; 16], Vec<u8>)> {
194 self.templates
195 .lock()
196 .unwrap()
197 .iter()
198 .map(|(h, b)| (*h, b.clone()))
199 .collect()
200 }
201
202 pub fn len(&self) -> usize {
204 self.templates.lock().unwrap().len()
205 }
206
207 pub fn is_empty(&self) -> bool {
209 self.len() == 0
210 }
211}
212
213#[derive(Debug, Default, Clone)]
218pub struct TemplateRegistry {
219 templates: HashMap<[u8; 16], Arc<Vec<u8>>>,
220}
221
222impl TemplateRegistry {
223 pub fn new() -> Self {
225 Self::default()
226 }
227
228 pub fn insert(&mut self, template: Vec<u8>) -> [u8; 16] {
230 let hash = blake3_128(&template);
231 self.templates.insert(hash, Arc::new(template));
232 hash
233 }
234
235 pub fn get(&self, hash: &[u8; 16]) -> Option<&[u8]> {
237 self.templates.get(hash).map(|t| t.as_slice())
238 }
239
240 pub fn iter(&self) -> impl Iterator<Item = (&[u8; 16], &[u8])> {
244 self.templates.iter().map(|(h, t)| (h, t.as_slice()))
245 }
246
247 pub fn len(&self) -> usize {
249 self.templates.len()
250 }
251
252 pub fn is_empty(&self) -> bool {
254 self.templates.is_empty()
255 }
256}
257
258#[derive(Debug, Default, Clone, Serialize, Deserialize)]
268pub struct TileMeta {
269 #[serde(skip_serializing_if = "Option::is_none")]
272 pub qa: Option<BTreeMap<String, (f64, f64)>>,
273 #[serde(skip_serializing_if = "Option::is_none")]
276 pub sorted: Option<bool>,
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub t0: Option<i64>,
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub vb: Option<u32>,
283 #[serde(skip_serializing_if = "Option::is_none")]
286 pub vt: Option<(i64, u32)>,
287}
288
289const GEOARROW_EXT_KEY: &str = "ARROW:extension:name";
291
292const GEOARROW_EXT_META_KEY: &str = "ARROW:extension:metadata";
295
296const GEOARROW_CRS_METADATA: &str = r#"{"crs":"OGC:CRS84","crs_type":"authority_code"}"#;
304
305pub type Coord = [f64; 2];
307
308#[derive(Debug, Clone)]
311pub enum GeometryColumn {
312 Point(Vec<Coord>),
314 LineString(Vec<Vec<Coord>>),
316 Polygon(Vec<Vec<Vec<Coord>>>),
318}
319
320impl GeometryColumn {
321 pub fn len(&self) -> usize {
323 match self {
324 GeometryColumn::Point(v) => v.len(),
325 GeometryColumn::LineString(v) => v.len(),
326 GeometryColumn::Polygon(v) => v.len(),
327 }
328 }
329
330 pub fn is_empty(&self) -> bool {
332 self.len() == 0
333 }
334
335 pub fn kind(&self) -> GeometryType {
337 match self {
338 GeometryColumn::Point(_) => GeometryType::Point,
339 GeometryColumn::LineString(_) => GeometryType::LineString,
340 GeometryColumn::Polygon(_) => GeometryType::Polygon,
341 }
342 }
343
344 fn geoarrow_name(&self) -> &'static str {
346 match self {
347 GeometryColumn::Point(_) => "geoarrow.point",
348 GeometryColumn::LineString(_) => "geoarrow.linestring",
349 GeometryColumn::Polygon(_) => "geoarrow.polygon",
350 }
351 }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum VectorElem {
358 F32,
360 U8,
362}
363
364#[derive(Debug, Clone)]
366pub enum PropertyColumn {
367 Numeric(Vec<Option<f64>>),
369 Categorical(Vec<Option<String>>),
371 Vector {
380 width: usize,
382 elem: VectorElem,
384 values: Vec<f32>,
387 },
388}
389
390#[derive(Debug, Clone)]
392pub struct ColumnarLayer {
393 pub name: String,
395 pub feature_ids: Vec<u64>,
397 pub start_times: Vec<i64>,
399 pub end_times: Vec<i64>,
401 pub geometry: GeometryColumn,
403 pub vertex_times: Option<Vec<Vec<i64>>>,
406 pub vertex_values: Option<Vec<Vec<f32>>>,
411 pub vertex_value_matrix: Option<Vec<Vec<f32>>>,
421 pub triangles: Option<Vec<Vec<u32>>>,
432 pub properties: Vec<(String, PropertyColumn)>,
434}
435
436impl ColumnarLayer {
437 pub fn feature_count(&self) -> usize {
439 self.feature_ids.len()
440 }
441
442 fn validate(&self) -> Result<()> {
444 let n = self.feature_ids.len();
445 let check = |label: &str, len: usize| -> Result<()> {
446 if len != n {
447 return Err(Error::Other(format!(
448 "tile layer '{}': {} has {} entries, expected {}",
449 self.name, label, len, n
450 )));
451 }
452 Ok(())
453 };
454 check("start_times", self.start_times.len())?;
455 check("end_times", self.end_times.len())?;
456 check("geometry", self.geometry.len())?;
457 if let Some(vt) = &self.vertex_times {
458 check("vertex_times", vt.len())?;
459 }
460 if let Some(vv) = &self.vertex_values {
461 check("vertex_values", vv.len())?;
462 }
463 if let Some(vm) = &self.vertex_value_matrix {
464 check("vertex_value_matrix", vm.len())?;
465 }
466 if let Some(tri) = &self.triangles {
467 check("triangles", tri.len())?;
468 }
469 for (name, col) in &self.properties {
470 match col {
471 PropertyColumn::Numeric(v) => check(&format!("property '{}'", name), v.len())?,
472 PropertyColumn::Categorical(v) => check(&format!("property '{}'", name), v.len())?,
473 PropertyColumn::Vector { width, values, .. } => {
474 if *width == 0 {
475 return Err(Error::Other(format!(
476 "tile layer '{}': vector property '{}' has width 0",
477 self.name, name
478 )));
479 }
480 if values.len() != width * n {
481 return Err(Error::Other(format!(
482 "tile layer '{}': vector property '{}' has {} values, expected {} ({} × {})",
483 self.name,
484 name,
485 values.len(),
486 width * n,
487 width,
488 n
489 )));
490 }
491 }
492 }
493 }
494 Ok(())
495 }
496}
497
498pub const TRIANGLES_METADATA_KEY: &str = "stt:has_triangles";
500
501pub fn tessellate_polygon(rings: &[Vec<Coord>]) -> Vec<u32> {
508 if rings.is_empty() {
509 return Vec::new();
510 }
511 let mut flat: Vec<f64> = Vec::with_capacity(rings.iter().map(|r| r.len()).sum::<usize>() * 2);
513 let mut hole_indices: Vec<usize> = Vec::with_capacity(rings.len().saturating_sub(1));
516 let mut running = 0usize;
517 for (i, ring) in rings.iter().enumerate() {
518 if i > 0 {
519 hole_indices.push(running);
520 }
521 for [x, y] in ring {
522 flat.push(*x);
523 flat.push(*y);
524 }
525 running += ring.len();
526 }
527 if running < 3 {
528 return Vec::new();
529 }
530 match earcutr::earcut(&flat, &hole_indices, 2) {
531 Ok(tris) => tris.into_iter().map(|i| i as u32).collect(),
532 Err(_) => Vec::new(),
533 }
534}
535
536fn offsets_from_counts(counts: impl Iterator<Item = usize>) -> Result<OffsetBuffer<i32>> {
547 let mut acc = 0i32;
548 let mut offsets = vec![0i32];
549 for c in counts {
550 acc = i32::try_from(c)
551 .ok()
552 .and_then(|c| acc.checked_add(c))
553 .ok_or_else(|| {
554 Error::Other(format!(
555 "layer geometry exceeds {} total vertices/rings, which Arrow's \
556 32-bit list offsets cannot address; split the layer",
557 i32::MAX
558 ))
559 })?;
560 offsets.push(acc);
561 }
562 Ok(OffsetBuffer::new(offsets.into()))
563}
564
565const M_PER_DEG_LAT: f64 = 111_320.0;
568
569pub const STT_QUANT_META_KEY: &str = "stt:quant";
573
574#[derive(Debug, Clone, Copy, PartialEq)]
581pub struct QuantAffine {
582 pub x0: f64,
583 pub y0: f64,
584 pub sx: f64,
585 pub sy: f64,
586 pub z0: Option<f64>,
590 pub sz: Option<f64>,
591}
592
593impl QuantAffine {
594 fn to_json(&self) -> String {
595 match (self.z0, self.sz) {
598 (Some(z0), Some(sz)) => format!(
599 r#"{{"x0":{:.17e},"y0":{:.17e},"sx":{:.17e},"sy":{:.17e},"z0":{:.17e},"sz":{:.17e}}}"#,
600 self.x0, self.y0, self.sx, self.sy, z0, sz
601 ),
602 _ => format!(
603 r#"{{"x0":{:.17e},"y0":{:.17e},"sx":{:.17e},"sy":{:.17e}}}"#,
604 self.x0, self.y0, self.sx, self.sy
605 ),
606 }
607 }
608
609 pub fn from_json(s: &str) -> Option<QuantAffine> {
612 let v: serde_json::Value = serde_json::from_str(s).ok()?;
613 let f = |k: &str| v.get(k).and_then(|x| x.as_f64());
614 Some(QuantAffine {
615 x0: f("x0")?,
616 y0: f("y0")?,
617 sx: f("sx")?,
618 sy: f("sy")?,
619 z0: f("z0"),
620 sz: f("sz"),
621 })
622 }
623
624 #[inline]
626 pub fn lon(&self, qx: i32) -> f64 {
627 self.x0 + qx as f64 * self.sx
628 }
629 #[inline]
631 pub fn lat(&self, qy: i32) -> f64 {
632 self.y0 + qy as f64 * self.sy
633 }
634
635 #[inline]
636 fn qx(&self, lon: f64) -> i32 {
637 (((lon - self.x0) / self.sx).round() as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32
638 }
639 #[inline]
640 fn qy(&self, lat: f64) -> i32 {
641 (((lat - self.y0) / self.sy).round() as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32
642 }
643 #[inline]
649 fn qz(&self, z: f64) -> Result<i32> {
650 let z0 = self.z0.unwrap_or(0.0);
651 let sz = self.sz.unwrap_or(1.0);
652 let q = ((z - z0) / sz).round();
653 if !(q >= i32::MIN as f64 && q <= i32::MAX as f64) {
654 return Err(Error::Other(format!(
655 "altitude {z} does not fit the quantization grid (origin {z0}, step {sz}): \
656 index {q} is outside i32; use a coarser --quantize-coords precision or drop \
657 the point-elevation fold for this dataset"
658 )));
659 }
660 Ok(q as i32)
661 }
662}
663
664pub const MIN_QUANTIZE_COORDS_M: f64 = 360.0 * M_PER_DEG_LAT / (i32::MAX as f64);
671
672pub fn validate_quantize_coords_m(meters: f64) -> Result<()> {
678 if meters > 0.0 && meters < MIN_QUANTIZE_COORDS_M {
679 return Err(Error::Other(format!(
680 "coordinate quantization precision {meters} m is finer than the minimum \
681 {MIN_QUANTIZE_COORDS_M} m (~19 mm): the world-anchored grid's ±180° longitude \
682 index would overflow i32 and snap points to wrong locations"
683 )));
684 }
685 Ok(())
686}
687
688fn world_grid_affine(meters: f64) -> Option<QuantAffine> {
702 if !(meters > 0.0) {
703 return None;
704 }
705 let step = meters / M_PER_DEG_LAT;
706 Some(QuantAffine {
707 x0: -180.0,
708 y0: -90.0,
709 sx: step,
710 sy: step,
711 z0: None,
712 sz: None,
713 })
714}
715
716fn world_grid_affine_3d(meters: f64) -> Option<QuantAffine> {
722 let a = world_grid_affine(meters)?;
723 Some(QuantAffine {
724 z0: Some(0.0),
725 sz: Some(meters),
726 ..a
727 })
728}
729
730fn build_geometry_array_q(
740 geom: &GeometryColumn,
741 quant: Option<&QuantAffine>,
742 point_elev: Option<&[f64]>,
743) -> Result<ArrayRef> {
744 if let (GeometryColumn::Point(points), Some(elev)) = (geom, point_elev) {
746 let list_size = 3;
747 let dt = if quant.is_some() { DataType::Int32 } else { DataType::Float64 };
748 let field = Arc::new(Field::new("xyz", dt, false));
749 let child: ArrayRef = match quant {
750 Some(q) => {
751 let mut iv = Vec::with_capacity(points.len() * 3);
752 for (i, [x, y]) in points.iter().enumerate() {
753 iv.push(q.qx(*x));
754 iv.push(q.qy(*y));
755 iv.push(q.qz(elev.get(i).copied().unwrap_or(0.0))?);
756 }
757 Arc::new(Int32Array::from(iv))
758 }
759 None => {
760 let mut flat = Vec::with_capacity(points.len() * 3);
761 for (i, [x, y]) in points.iter().enumerate() {
762 flat.push(*x);
763 flat.push(*y);
764 flat.push(elev.get(i).copied().unwrap_or(0.0));
765 }
766 Arc::new(Float64Array::from(flat))
767 }
768 };
769 return Ok(Arc::new(FixedSizeListArray::new(field, list_size, child, None)));
770 }
771
772 let coord_field = || {
773 let dt = if quant.is_some() {
774 DataType::Int32
775 } else {
776 DataType::Float64
777 };
778 Arc::new(Field::new("xy", dt, false))
779 };
780 let make_leaf = |flat: Vec<f64>| -> ArrayRef {
783 match quant {
784 Some(q) => {
785 let mut iv = Vec::with_capacity(flat.len());
786 let mut i = 0;
787 while i + 1 < flat.len() {
788 iv.push(q.qx(flat[i]));
789 iv.push(q.qy(flat[i + 1]));
790 i += 2;
791 }
792 Arc::new(FixedSizeListArray::new(
793 coord_field(),
794 2,
795 Arc::new(Int32Array::from(iv)),
796 None,
797 ))
798 }
799 None => Arc::new(FixedSizeListArray::new(
800 coord_field(),
801 2,
802 Arc::new(Float64Array::from(flat)),
803 None,
804 )),
805 }
806 };
807
808 Ok(match geom {
809 GeometryColumn::Point(points) => {
810 let mut flat = Vec::with_capacity(points.len() * 2);
811 for [x, y] in points {
812 flat.push(*x);
813 flat.push(*y);
814 }
815 make_leaf(flat)
816 }
817 GeometryColumn::LineString(lines) => {
818 let mut flat: Vec<f64> = Vec::new();
819 for line in lines {
820 for [x, y] in line {
821 flat.push(*x);
822 flat.push(*y);
823 }
824 }
825 let coords = make_leaf(flat);
826 let offsets = offsets_from_counts(lines.iter().map(|l| l.len()))?;
827 let vertex_field = Arc::new(Field::new("vertices", coords.data_type().clone(), false));
828 Arc::new(ListArray::new(vertex_field, offsets, coords, None))
829 }
830 GeometryColumn::Polygon(polys) => {
831 let mut flat: Vec<f64> = Vec::new();
833 let mut ring_sizes: Vec<usize> = Vec::new();
834 let mut rings_per_feature: Vec<usize> = Vec::new();
835 for feature in polys {
836 rings_per_feature.push(feature.len());
837 for ring in feature {
838 ring_sizes.push(ring.len());
839 for [x, y] in ring {
840 flat.push(*x);
841 flat.push(*y);
842 }
843 }
844 }
845 let coords = make_leaf(flat);
846 let ring_offsets = offsets_from_counts(ring_sizes.into_iter())?;
848 let vertex_field = Arc::new(Field::new("vertices", coords.data_type().clone(), false));
849 let rings: ArrayRef = Arc::new(ListArray::new(
850 vertex_field,
851 ring_offsets,
852 coords,
853 None,
854 ));
855 let feature_offsets = offsets_from_counts(rings_per_feature.into_iter())?;
857 let ring_field = Arc::new(Field::new("rings", rings.data_type().clone(), false));
858 Arc::new(ListArray::new(ring_field, feature_offsets, rings, None))
859 }
860 })
861}
862
863const VERTEX_TIME_ORIGIN_KEY: &str = "stt:vertex_time_origin_ms";
865const VERTEX_TIME_STEP_KEY: &str = "stt:vertex_time_step_ms";
866const VERTEX_VALUE_BUCKETS_KEY: &str = "stt:vertex_value_buckets";
870const TIME_OFFSET_MS_KEY: &str = "stt:time_offset_ms";
876
877pub const DEFAULT_VERTEX_TIME_MAX_STEP_MS: u32 = 1000;
886
887static VERTEX_TIME_MAX_STEP_MS: AtomicU32 = AtomicU32::new(DEFAULT_VERTEX_TIME_MAX_STEP_MS);
891
892static VERTEX_TIME_FALLBACK_WARNED: AtomicBool = AtomicBool::new(false);
895
896pub fn set_vertex_time_max_step_ms(ms: u32) {
900 VERTEX_TIME_MAX_STEP_MS.store(ms.max(1), Ordering::Relaxed);
901}
902
903pub fn vertex_time_max_step_ms() -> u32 {
905 VERTEX_TIME_MAX_STEP_MS.load(Ordering::Relaxed)
906}
907
908static QUANTIZE_COORDS_UM: AtomicU32 = AtomicU32::new(0);
914
915pub fn set_quantize_coords_m(meters: f64) -> Result<()> {
922 validate_quantize_coords_m(meters)?;
923 let um = if meters > 0.0 {
924 (meters * 1.0e6).round().clamp(1.0, u32::MAX as f64) as u32
925 } else {
926 0
927 };
928 QUANTIZE_COORDS_UM.store(um, Ordering::Relaxed);
929 Ok(())
930}
931
932pub fn quantize_coords_m() -> Option<f64> {
934 let um = QUANTIZE_COORDS_UM.load(Ordering::Relaxed);
935 (um > 0).then(|| um as f64 / 1.0e6)
936}
937
938pub const STT_QUANT_ATTR_META_KEY: &str = "stt:qa";
944
945#[derive(Debug, Clone, Copy, PartialEq)]
950pub struct AttrQuant {
951 pub o: f64,
952 pub s: f64,
953}
954
955impl AttrQuant {
956 fn to_json(&self) -> String {
957 format!(r#"{{"o":{:.17e},"s":{:.17e}}}"#, self.o, self.s)
959 }
960
961 pub fn from_json(s: &str) -> Option<AttrQuant> {
963 let v: serde_json::Value = serde_json::from_str(s).ok()?;
964 Some(AttrQuant {
965 o: v.get("o")?.as_f64()?,
966 s: v.get("s")?.as_f64()?,
967 })
968 }
969
970 #[inline]
972 pub fn value(&self, q: i64) -> f64 {
973 self.o + q as f64 * self.s
974 }
975}
976
977fn quant_attrs_cell() -> &'static RwLock<HashMap<String, f64>> {
983 static A: OnceLock<RwLock<HashMap<String, f64>>> = OnceLock::new();
984 A.get_or_init(|| RwLock::new(HashMap::new()))
985}
986
987pub fn set_quantize_attrs(map: HashMap<String, f64>) {
989 *quant_attrs_cell().write().unwrap() = map;
990}
991
992pub fn quantize_attrs() -> HashMap<String, f64> {
994 quant_attrs_cell().read().unwrap().clone()
995}
996
997
998static QUANTIZE_ATTRS_AUTO: AtomicBool = AtomicBool::new(false);
1007
1008pub fn set_quantize_attrs_auto(on: bool) {
1012 QUANTIZE_ATTRS_AUTO.store(on, Ordering::Relaxed);
1013}
1014
1015pub fn quantize_attrs_auto() -> bool {
1017 QUANTIZE_ATTRS_AUTO.load(Ordering::Relaxed)
1018}
1019
1020#[derive(Debug, Clone)]
1027pub struct VectorGroup {
1028 pub name: String,
1030 pub components: Vec<String>,
1032 pub elem: VectorElem,
1034}
1035
1036fn vector_groups_cell() -> &'static RwLock<Vec<VectorGroup>> {
1040 static A: OnceLock<RwLock<Vec<VectorGroup>>> = OnceLock::new();
1041 A.get_or_init(|| RwLock::new(Vec::new()))
1042}
1043
1044pub fn set_vector_groups(groups: Vec<VectorGroup>) {
1046 *vector_groups_cell().write().unwrap() = groups;
1047}
1048
1049pub fn vector_groups() -> Vec<VectorGroup> {
1051 vector_groups_cell().read().unwrap().clone()
1052}
1053
1054fn point_elevation_column_cell() -> &'static RwLock<String> {
1060 static A: OnceLock<RwLock<String>> = OnceLock::new();
1061 A.get_or_init(|| RwLock::new(String::new()))
1062}
1063
1064pub fn set_point_elevation_column(name: &str) {
1066 *point_elevation_column_cell().write().unwrap() = name.to_string();
1067}
1068
1069pub fn point_elevation_column() -> String {
1071 point_elevation_column_cell().read().unwrap().clone()
1072}
1073
1074static FORMAT_VERSION: AtomicU32 = AtomicU32::new(FORMAT_VERSION_V1);
1080
1081pub fn set_format_version(v: u32) -> Result<()> {
1084 if v != FORMAT_VERSION_V1 && v != FORMAT_VERSION_V2 {
1085 return Err(Error::Other(format!(
1086 "unsupported format version {v} (this writer emits 1 or 2)"
1087 )));
1088 }
1089 FORMAT_VERSION.store(v, Ordering::Relaxed);
1090 Ok(())
1091}
1092
1093pub fn format_version() -> u32 {
1095 FORMAT_VERSION.load(Ordering::Relaxed)
1096}
1097
1098fn template_collector_cell() -> &'static RwLock<Option<Arc<TemplateCollector>>> {
1103 static A: OnceLock<RwLock<Option<Arc<TemplateCollector>>>> = OnceLock::new();
1104 A.get_or_init(|| RwLock::new(None))
1105}
1106
1107pub fn set_template_collector(collector: Option<Arc<TemplateCollector>>) {
1109 *template_collector_cell().write().unwrap() = collector;
1110}
1111
1112pub fn template_collector() -> Option<Arc<TemplateCollector>> {
1114 template_collector_cell().read().unwrap().clone()
1115}
1116
1117#[derive(Debug, Clone)]
1131pub struct EncoderConfig {
1132 pub quantize_coords_m: Option<f64>,
1135 pub quantize_attrs: HashMap<String, f64>,
1137 pub quantize_attrs_auto: bool,
1139 pub vector_groups: Vec<VectorGroup>,
1141 pub point_elevation_column: String,
1143 pub vertex_time_max_step_ms: u32,
1145 pub format_version: u32,
1151 pub template_collector: Option<Arc<TemplateCollector>>,
1157}
1158
1159impl Default for EncoderConfig {
1160 fn default() -> Self {
1161 Self {
1162 quantize_coords_m: None,
1163 quantize_attrs: HashMap::new(),
1164 quantize_attrs_auto: false,
1165 vector_groups: Vec::new(),
1166 point_elevation_column: String::new(),
1167 vertex_time_max_step_ms: DEFAULT_VERTEX_TIME_MAX_STEP_MS,
1168 format_version: FORMAT_VERSION_V1,
1169 template_collector: None,
1170 }
1171 }
1172}
1173
1174impl EncoderConfig {
1175 pub fn from_globals() -> Self {
1178 Self {
1179 quantize_coords_m: quantize_coords_m(),
1180 quantize_attrs: quantize_attrs(),
1181 quantize_attrs_auto: quantize_attrs_auto(),
1182 vector_groups: vector_groups(),
1183 point_elevation_column: point_elevation_column(),
1184 vertex_time_max_step_ms: vertex_time_max_step_ms(),
1185 format_version: format_version(),
1186 template_collector: template_collector(),
1187 }
1188 }
1189}
1190
1191fn group_vector_properties(
1200 props: &[(String, PropertyColumn)],
1201 n: usize,
1202 groups: &[VectorGroup],
1203) -> Option<Vec<(String, PropertyColumn)>> {
1204 if groups.is_empty() {
1205 return None;
1206 }
1207 let numeric: HashMap<&str, &[Option<f64>]> = props
1209 .iter()
1210 .filter_map(|(k, c)| match c {
1211 PropertyColumn::Numeric(v) => Some((k.as_str(), v.as_slice())),
1212 _ => None,
1213 })
1214 .collect();
1215
1216 let mut out: Vec<(String, PropertyColumn)> = Vec::new();
1217 let mut consumed: HashSet<String> = HashSet::new();
1218 let mut any = false;
1219 for g in groups {
1220 if g.components.is_empty()
1221 || !g.components.iter().all(|c| numeric.contains_key(c.as_str()))
1222 {
1223 continue;
1224 }
1225 let width = g.components.len();
1226 let mut values = vec![0f32; width * n];
1227 for (ci, cname) in g.components.iter().enumerate() {
1228 let col = numeric[cname.as_str()];
1229 for i in 0..n {
1230 values[i * width + ci] = col[i].map(|x| x as f32).unwrap_or(0.0);
1231 }
1232 }
1233 for c in &g.components {
1234 consumed.insert(c.clone());
1235 }
1236 out.push((
1237 g.name.clone(),
1238 PropertyColumn::Vector {
1239 width,
1240 elem: g.elem,
1241 values,
1242 },
1243 ));
1244 any = true;
1245 }
1246 if !any {
1247 return None;
1248 }
1249 for (k, c) in props {
1251 if !consumed.contains(k) {
1252 out.push((k.clone(), c.clone()));
1253 }
1254 }
1255 Some(out)
1256}
1257
1258fn build_quantized_numeric_auto(values: &[Option<f64>]) -> Option<(ArrayRef, String)> {
1267 let mut min = f64::INFINITY;
1268 let mut max = f64::NEG_INFINITY;
1269 for v in values.iter().flatten() {
1270 if v.is_finite() {
1271 min = min.min(*v);
1272 max = max.max(*v);
1273 }
1274 }
1275 let (o, s) = if min.is_finite() {
1279 if max > min {
1280 (min, (max - min) / u16::MAX as f64)
1281 } else {
1282 (min, 1.0)
1283 }
1284 } else {
1285 (0.0, 1.0)
1286 };
1287 let affine = AttrQuant { o, s };
1288 let mut b = UInt16Builder::with_capacity(values.len());
1289 for v in values {
1290 match v {
1291 Some(x) if x.is_finite() => {
1292 let q = (((*x - o) / s).round()).clamp(0.0, u16::MAX as f64) as u16;
1293 b.append_value(q);
1294 }
1295 _ => b.append_null(),
1296 }
1297 }
1298 Some((Arc::new(b.finish()), affine.to_json()))
1299}
1300
1301fn build_quantized_numeric(
1316 values: &[Option<f64>],
1317 prec: f64,
1318) -> Result<Option<(ArrayRef, String)>> {
1319 if !(prec > 0.0) {
1320 return Ok(None);
1321 }
1322 let mut min = f64::INFINITY;
1323 for v in values.iter().flatten() {
1324 if v.is_finite() && *v < min {
1325 min = *v;
1326 }
1327 }
1328 if !min.is_finite() {
1329 return Ok(None); }
1331 let affine = AttrQuant { o: min, s: prec };
1332 let mut q: Vec<Option<i64>> = Vec::with_capacity(values.len());
1333 let mut max_q: i64 = 0;
1334 for v in values {
1335 match v {
1336 Some(x) if x.is_finite() => {
1337 let qi = (((*x - affine.o) / affine.s).round() as i64).max(0);
1338 if qi > i32::MAX as i64 {
1339 return Err(Error::Other(format!(
1340 "numeric property quantization overflows: value {x} at precision \
1341 {prec} quantizes to index {qi} (offset {min}), beyond the Int32 \
1342 leaf's {} ceiling; use a coarser --quantize-attr precision or \
1343 leave the column Float64",
1344 i32::MAX
1345 )));
1346 }
1347 if qi > max_q {
1348 max_q = qi;
1349 }
1350 q.push(Some(qi));
1351 }
1352 _ => q.push(None),
1353 }
1354 }
1355 let array: ArrayRef = if max_q <= u16::MAX as i64 {
1356 let mut b = UInt16Builder::with_capacity(q.len());
1357 for qi in &q {
1358 match qi {
1359 Some(v) => b.append_value(*v as u16),
1360 None => b.append_null(),
1361 }
1362 }
1363 Arc::new(b.finish())
1364 } else {
1365 let mut b = Int32Builder::with_capacity(q.len());
1366 for qi in &q {
1367 match qi {
1368 Some(v) => b.append_value(*v as i32),
1369 None => b.append_null(),
1370 }
1371 }
1372 Arc::new(b.finish())
1373 };
1374 Ok(Some((array, affine.to_json())))
1375}
1376
1377fn build_dictionary_indices(
1390 values: &[Option<String>],
1391) -> Result<(Vec<Option<u16>>, Vec<String>)> {
1392 let mut categories: Vec<String> = Vec::new();
1393 let mut lookup: HashMap<String, u16> = HashMap::new();
1394 let mut indices: Vec<Option<u16>> = Vec::with_capacity(values.len());
1395 for v in values {
1396 match v {
1397 Some(s) => {
1398 if let Some(&idx) = lookup.get(s) {
1399 indices.push(Some(idx));
1400 } else if categories.len() < u16::MAX as usize {
1401 let idx = categories.len() as u16;
1402 categories.push(s.clone());
1403 lookup.insert(s.clone(), idx);
1404 indices.push(Some(idx));
1405 } else {
1406 return Err(Error::Other(format!(
1407 "categorical column has more than {} distinct values, which a \
1408 Dictionary<UInt16, Utf8> key cannot address; split the column \
1409 into multiple categorical fields or widen the key type",
1410 u16::MAX
1411 )));
1412 }
1413 }
1414 None => indices.push(None),
1415 }
1416 }
1417 Ok((indices, categories))
1418}
1419
1420struct VertexTimeColumn {
1423 array: ArrayRef,
1424 encoding: Option<(i64, u32)>,
1429}
1430
1431fn build_vertex_time_array(
1441 vertex_times: &Option<Vec<Vec<i64>>>,
1442 feature_count: usize,
1443 max_step_ms: u32,
1444) -> Option<VertexTimeColumn> {
1445 let vt = vertex_times.as_ref()?;
1446
1447 let mut min = i64::MAX;
1451 let mut max = i64::MIN;
1452 let mut any = false;
1453 for times in vt.iter().take(feature_count) {
1454 for &t in times {
1455 if t < min {
1456 min = t;
1457 }
1458 if t > max {
1459 max = t;
1460 }
1461 any = true;
1462 }
1463 }
1464
1465 if any && max >= min {
1466 let span = (max - min) as u64;
1470 let step = if span <= u16::MAX as u64 {
1473 1u64
1474 } else {
1475 ((span + u16::MAX as u64 - 1) / u16::MAX as u64).max(1)
1476 };
1477 if step <= max_step_ms as u64 {
1480 let step = step as u32;
1481 let mut builder = ListBuilder::new(UInt16Builder::new());
1482 for i in 0..feature_count {
1483 match vt.get(i) {
1484 Some(times) if !times.is_empty() => {
1485 for &t in times {
1486 let delta = ((t - min) as u64 / step as u64).min(u16::MAX as u64) as u16;
1491 builder.values().append_value(delta);
1492 }
1493 builder.append(true);
1494 }
1495 _ => builder.append(false),
1496 }
1497 }
1498 return Some(VertexTimeColumn {
1499 array: Arc::new(builder.finish()),
1500 encoding: Some((min, step)),
1501 });
1502 }
1503 if !VERTEX_TIME_FALLBACK_WARNED.swap(true, Ordering::Relaxed) {
1506 tracing::warn!(
1507 "vertex-time span {}ms exceeds u16-delta ceiling (step {}ms > max {}ms); \
1508 falling back to exact Int64 — payload keeps full precision but is ~4x larger",
1509 span,
1510 step,
1511 max_step_ms
1512 );
1513 }
1514 }
1515
1516 let mut builder = ListBuilder::new(Int64Builder::new());
1519 for i in 0..feature_count {
1520 match vt.get(i) {
1521 Some(times) if !times.is_empty() => {
1522 for &t in times {
1523 builder.values().append_value(t);
1524 }
1525 builder.append(true);
1526 }
1527 _ => builder.append(false),
1528 }
1529 }
1530 Some(VertexTimeColumn {
1531 array: Arc::new(builder.finish()),
1532 encoding: None,
1533 })
1534}
1535
1536fn build_vertex_value_array(
1544 vertex_values: &Option<Vec<Vec<f32>>>,
1545 feature_count: usize,
1546) -> Option<ArrayRef> {
1547 let vv = vertex_values.as_ref()?;
1548 let any = vv.iter().take(feature_count).any(|v| !v.is_empty());
1549 if !any {
1550 return None;
1551 }
1552 let mut builder = ListBuilder::new(Float32Builder::new());
1553 for i in 0..feature_count {
1554 match vv.get(i) {
1555 Some(values) if !values.is_empty() => {
1556 for &v in values {
1557 builder.values().append_value(v);
1558 }
1559 builder.append(true);
1560 }
1561 _ => builder.append(false),
1562 }
1563 }
1564 Some(Arc::new(builder.finish()))
1565}
1566
1567fn infer_vertex_value_buckets(matrix: &[Vec<f32>], geometry: &GeometryColumn) -> Option<u32> {
1573 let lines = match geometry {
1574 GeometryColumn::LineString(lines) => lines,
1575 _ => return None,
1576 };
1577 for (i, m) in matrix.iter().enumerate() {
1578 let nv = lines.get(i)?.len();
1579 if !m.is_empty() && nv > 0 && m.len() % nv == 0 {
1580 return Some((m.len() / nv) as u32);
1581 }
1582 }
1583 None
1584}
1585
1586pub fn encode_layer(layer: &ColumnarLayer) -> Result<Vec<u8>> {
1592 encode_layer_cfg(layer, &EncoderConfig::from_globals())
1593}
1594
1595pub fn encode_layer_quantized(layer: &ColumnarLayer, quantize_m: Option<f64>) -> Result<Vec<u8>> {
1608 encode_layer_cfg(
1609 layer,
1610 &EncoderConfig {
1611 quantize_coords_m: quantize_m,
1612 ..EncoderConfig::from_globals()
1613 },
1614 )
1615}
1616
1617pub fn encode_layer_with(layer: &ColumnarLayer, cfg: &EncoderConfig) -> Result<Vec<u8>> {
1621 encode_layer_cfg(layer, cfg)
1622}
1623
1624fn encode_layer_cfg(layer: &ColumnarLayer, cfg: &EncoderConfig) -> Result<Vec<u8>> {
1632 let parts = build_layer_parts(layer, cfg)?;
1633 assemble_layer_ipc_v1(parts)
1634}
1635
1636struct LayerParts {
1642 fields: Vec<Arc<Field>>,
1643 columns: Vec<ArrayRef>,
1644 reserved_len: usize,
1647 layer_name: String,
1648 geometry_name: &'static str,
1649 min_start_time: Option<i64>,
1651 vertex_time_encoding: Option<(i64, u32)>,
1653 vertex_value_buckets: Option<u32>,
1655 has_triangles: bool,
1656}
1657
1658fn build_layer_parts(layer: &ColumnarLayer, cfg: &EncoderConfig) -> Result<LayerParts> {
1662 layer.validate()?;
1663 let n = layer.feature_count();
1664
1665 let mut fields: Vec<Arc<Field>> = Vec::new();
1666 let mut columns: Vec<ArrayRef> = Vec::new();
1667
1668 fields.push(Arc::new(Field::new("id", DataType::UInt64, false)));
1669 columns.push(Arc::new(UInt64Array::from(layer.feature_ids.clone())));
1670
1671 fields.push(Arc::new(Field::new("start_time", DataType::Int64, false)));
1672 columns.push(Arc::new(Int64Array::from(layer.start_times.clone())));
1673
1674 fields.push(Arc::new(Field::new("end_time", DataType::Int64, false)));
1675 columns.push(Arc::new(Int64Array::from(layer.end_times.clone())));
1676
1677 let elev_col = cfg.point_elevation_column.clone();
1681 let point_elev: Option<Vec<f64>> = if !elev_col.is_empty()
1682 && matches!(layer.geometry, GeometryColumn::Point(_))
1683 {
1684 layer.properties.iter().find_map(|(name, col)| match col {
1685 PropertyColumn::Numeric(v) if name == &elev_col => {
1686 let n = layer.feature_count();
1687 let mut out = vec![0.0f64; n];
1688 for (i, x) in v.iter().enumerate() {
1689 if let Some(val) = x {
1690 out[i] = *val;
1691 }
1692 }
1693 Some(out)
1694 }
1695 _ => None,
1696 })
1697 } else {
1698 None
1699 };
1700 let elev_consumed = point_elev.is_some();
1701
1702 if let Some(m) = cfg.quantize_coords_m {
1707 validate_quantize_coords_m(m)?;
1708 }
1709 let quant = cfg.quantize_coords_m.and_then(|m| {
1710 if point_elev.is_some() {
1711 world_grid_affine_3d(m)
1712 } else {
1713 world_grid_affine(m)
1714 }
1715 });
1716 let geom_array =
1717 build_geometry_array_q(&layer.geometry, quant.as_ref(), point_elev.as_deref())?;
1718 let mut geom_meta = BTreeMap::new();
1725 geom_meta.insert(
1726 GEOARROW_EXT_KEY.to_string(),
1727 layer.geometry.geoarrow_name().to_string(),
1728 );
1729 match &quant {
1730 Some(q) => {
1738 geom_meta.insert(STT_QUANT_META_KEY.to_string(), q.to_json());
1739 }
1740 None => {
1742 geom_meta.insert(
1743 GEOARROW_EXT_META_KEY.to_string(),
1744 GEOARROW_CRS_METADATA.to_string(),
1745 );
1746 }
1747 }
1748 fields.push(Arc::new(
1749 Field::new("geometry", geom_array.data_type().clone(), false)
1750 .with_metadata(geom_meta.into_iter().collect()),
1751 ));
1752 columns.push(geom_array);
1753
1754 let mut vertex_time_encoding: Option<(i64, u32)> = None;
1757 if let Some(vt_col) = build_vertex_time_array(&layer.vertex_times, n, cfg.vertex_time_max_step_ms) {
1758 fields.push(Arc::new(Field::new(
1759 "vertex_time",
1760 vt_col.array.data_type().clone(),
1761 true,
1762 )));
1763 columns.push(vt_col.array);
1764 vertex_time_encoding = vt_col.encoding;
1765 }
1766
1767 if let Some(vv_array) = build_vertex_value_array(&layer.vertex_values, n) {
1770 fields.push(Arc::new(Field::new(
1771 "vertex_value",
1772 vv_array.data_type().clone(),
1773 true,
1774 )));
1775 columns.push(vv_array);
1776 }
1777
1778 let mut vertex_value_buckets: Option<u32> = None;
1783 if let Some(vm_array) = build_vertex_value_array(&layer.vertex_value_matrix, n) {
1784 fields.push(Arc::new(Field::new(
1785 "vertex_value_matrix",
1786 vm_array.data_type().clone(),
1787 true,
1788 )));
1789 columns.push(vm_array);
1790 vertex_value_buckets = layer
1791 .vertex_value_matrix
1792 .as_ref()
1793 .and_then(|vm| infer_vertex_value_buckets(vm, &layer.geometry));
1794 }
1795
1796 let has_triangles = matches!(layer.geometry, GeometryColumn::Polygon(_))
1800 && layer
1801 .triangles
1802 .as_ref()
1803 .map(|t| t.iter().any(|f| !f.is_empty()))
1804 .unwrap_or(false);
1805 if has_triangles {
1806 let tri = layer.triangles.as_ref().unwrap();
1807 let max_index = tri.iter().flatten().copied().max().unwrap_or(0);
1816 let array: ArrayRef = if max_index <= u16::MAX as u32 {
1817 let mut builder = ListBuilder::new(UInt16Builder::new());
1818 for feature in tri {
1819 for &idx in feature {
1820 builder.values().append_value(idx as u16);
1821 }
1822 builder.append(true);
1825 }
1826 Arc::new(builder.finish())
1827 } else {
1828 let mut builder = ListBuilder::new(UInt32Builder::new());
1829 for feature in tri {
1830 for &idx in feature {
1831 builder.values().append_value(idx);
1832 }
1833 builder.append(true);
1834 }
1835 Arc::new(builder.finish())
1836 };
1837 fields.push(Arc::new(Field::new(
1838 "triangles",
1839 array.data_type().clone(),
1840 false,
1841 )));
1842 columns.push(array);
1843 }
1844
1845 let reserved_len = fields.len();
1848
1849 let grouped =
1854 group_vector_properties(&layer.properties, layer.feature_count(), &cfg.vector_groups);
1855 let props_iter: &[(String, PropertyColumn)] =
1856 grouped.as_deref().unwrap_or(&layer.properties);
1857 for (name, col) in props_iter {
1858 if elev_consumed && name == &elev_col {
1861 continue;
1862 }
1863 match col {
1864 PropertyColumn::Numeric(values) => {
1865 let quantized = match cfg
1872 .quantize_attrs
1873 .get(name)
1874 .copied()
1875 .filter(|p| *p > 0.0)
1876 {
1877 Some(p) => build_quantized_numeric(values, p)?,
1878 None => None,
1879 }
1880 .or_else(|| {
1881 cfg.quantize_attrs_auto
1884 .then(|| build_quantized_numeric_auto(values))
1885 .flatten()
1886 });
1887 match quantized {
1888 Some((array, affine_json)) => {
1889 let mut m = HashMap::new();
1890 m.insert(STT_QUANT_ATTR_META_KEY.to_string(), affine_json);
1891 fields.push(Arc::new(
1892 Field::new(name, array.data_type().clone(), true).with_metadata(m),
1893 ));
1894 columns.push(array);
1895 }
1896 None => {
1897 fields.push(Arc::new(Field::new(name, DataType::Float64, true)));
1898 columns.push(Arc::new(Float64Array::from(values.clone())));
1899 }
1900 }
1901 }
1902 PropertyColumn::Categorical(values) => {
1903 let (indices, categories) = build_dictionary_indices(values)?;
1907 let key_type = DataType::UInt16;
1908 let value_type = DataType::Utf8;
1909 let dict_type = DataType::Dictionary(Box::new(key_type), Box::new(value_type));
1910 fields.push(Arc::new(Field::new(name, dict_type, true)));
1911
1912 let value_array: ArrayRef = Arc::new(StringArray::from(
1913 categories.iter().map(|s| Some(s.as_str())).collect::<Vec<_>>(),
1914 ));
1915 let key_array = UInt16Array::from(indices);
1916 let dict = DictionaryArray::<UInt16Type>::try_new(key_array, value_array)
1917 .map_err(|e| Error::Other(format!("dictionary build failed: {e}")))?;
1918 columns.push(Arc::new(dict));
1919 }
1920 PropertyColumn::Vector { width, elem, values } => {
1921 let (child, child_dt): (ArrayRef, DataType) = match elem {
1927 VectorElem::F32 => (
1928 Arc::new(Float32Array::from(values.clone())),
1929 DataType::Float32,
1930 ),
1931 VectorElem::U8 => {
1932 let bytes: Vec<u8> = values
1933 .iter()
1934 .map(|v| v.round().clamp(0.0, 255.0) as u8)
1935 .collect();
1936 (Arc::new(UInt8Array::from(bytes)), DataType::UInt8)
1937 }
1938 };
1939 let item_field = Arc::new(Field::new("item", child_dt, false));
1940 let width_i32 = i32::try_from(*width).map_err(|_| {
1941 Error::Other(format!(
1942 "vector property '{name}' has width {width}, which exceeds the \
1943 FixedSizeList i32 size limit"
1944 ))
1945 })?;
1946 let fsl = FixedSizeListArray::new(item_field, width_i32, child, None);
1947 fields.push(Arc::new(Field::new(
1948 name,
1949 fsl.data_type().clone(),
1950 true,
1951 )));
1952 columns.push(Arc::new(fsl));
1953 }
1954 }
1955 }
1956
1957 Ok(LayerParts {
1958 fields,
1959 columns,
1960 reserved_len,
1961 layer_name: layer.name.clone(),
1962 geometry_name: layer.geometry.geoarrow_name(),
1963 min_start_time: layer.start_times.iter().copied().min(),
1970 vertex_time_encoding,
1971 vertex_value_buckets,
1972 has_triangles,
1973 })
1974}
1975
1976fn write_ipc_stream(schema: Arc<Schema>, columns: Vec<ArrayRef>) -> Result<Vec<u8>> {
1978 let batch = RecordBatch::try_new(schema.clone(), columns)
1979 .map_err(|e| Error::Other(format!("failed to build tile RecordBatch: {e}")))?;
1980 let mut buf = Vec::new();
1981 {
1982 let mut writer = StreamWriter::try_new(&mut buf, &schema)
1983 .map_err(|e| Error::Other(format!("Arrow IPC writer init failed: {e}")))?;
1984 writer
1985 .write(&batch)
1986 .map_err(|e| Error::Other(format!("Arrow IPC write failed: {e}")))?;
1987 writer
1988 .finish()
1989 .map_err(|e| Error::Other(format!("Arrow IPC finish failed: {e}")))?;
1990 }
1991 Ok(buf)
1992}
1993
1994fn assemble_layer_ipc_v1(parts: LayerParts) -> Result<Vec<u8>> {
1998 let mut schema_meta: BTreeMap<String, String> = BTreeMap::new();
2011 schema_meta.insert("stt:layer".to_string(), parts.layer_name.clone());
2012 schema_meta.insert("stt:geometry".to_string(), parts.geometry_name.to_string());
2013 if let Some(min_start) = parts.min_start_time {
2014 schema_meta.insert(TIME_OFFSET_MS_KEY.to_string(), min_start.to_string());
2015 }
2016 if let Some((origin, step)) = parts.vertex_time_encoding {
2017 schema_meta.insert(VERTEX_TIME_ORIGIN_KEY.to_string(), origin.to_string());
2018 schema_meta.insert(VERTEX_TIME_STEP_KEY.to_string(), step.to_string());
2019 }
2020 if let Some(buckets) = parts.vertex_value_buckets {
2021 schema_meta.insert(VERTEX_VALUE_BUCKETS_KEY.to_string(), buckets.to_string());
2022 }
2023 if parts.has_triangles {
2024 schema_meta.insert(TRIANGLES_METADATA_KEY.to_string(), "true".to_string());
2025 }
2026 let schema = Arc::new(
2027 Schema::new(parts.fields).with_metadata(schema_meta.into_iter().collect()),
2028 );
2029 write_ipc_stream(schema, parts.columns)
2030}
2031
2032struct EncodedLayerV2 {
2039 core_template: Vec<u8>,
2040 core_tail: Vec<u8>,
2041 props: Option<(Vec<u8>, Vec<u8>)>,
2044 tile_meta_json: String,
2045}
2046
2047fn split_ipc_at_schema(ipc: &[u8]) -> Result<usize> {
2054 if ipc.len() < 8 || ipc[0..4] != [0xFF, 0xFF, 0xFF, 0xFF] {
2055 return Err(Error::Other(
2056 "layer IPC stream does not start with an encapsulated message".into(),
2057 ));
2058 }
2059 let meta_len = i32::from_le_bytes(ipc[4..8].try_into().expect("4 bytes"));
2060 if meta_len <= 0 {
2061 return Err(Error::Other(
2062 "layer IPC stream starts with an end-of-stream marker".into(),
2063 ));
2064 }
2065 let boundary = 8usize
2066 .checked_add(meta_len as usize)
2067 .filter(|b| *b <= ipc.len())
2068 .ok_or_else(|| Error::Other("layer IPC schema message overruns the stream".into()))?;
2069 let msg = root_as_message(&ipc[8..boundary])
2070 .map_err(|e| Error::Other(format!("layer IPC schema flatbuffer parse failed: {e}")))?;
2071 if msg.header_type() != MessageHeader::Schema {
2072 return Err(Error::Other(format!(
2073 "layer IPC stream must start with a Schema message, got {:?}",
2074 msg.header_type()
2075 )));
2076 }
2077 if msg.bodyLength() != 0 {
2078 return Err(Error::Other(
2079 "layer IPC schema message unexpectedly carries a body".into(),
2080 ));
2081 }
2082 Ok(boundary)
2083}
2084
2085fn sort_rows_by_start_time(layer: &ColumnarLayer) -> Cow<'_, ColumnarLayer> {
2091 if layer.start_times.windows(2).all(|w| w[0] <= w[1]) {
2092 return Cow::Borrowed(layer);
2093 }
2094 let mut idx: Vec<usize> = (0..layer.feature_count()).collect();
2095 idx.sort_by_key(|&i| layer.start_times[i]); fn permute_nested<T: Clone>(v: &Option<Vec<Vec<T>>>, idx: &[usize]) -> Option<Vec<Vec<T>>> {
2101 v.as_ref()
2102 .map(|v| idx.iter().map(|&i| v.get(i).cloned().unwrap_or_default()).collect())
2103 }
2104
2105 let geometry = match &layer.geometry {
2106 GeometryColumn::Point(v) => {
2107 GeometryColumn::Point(idx.iter().map(|&i| v[i]).collect())
2108 }
2109 GeometryColumn::LineString(v) => {
2110 GeometryColumn::LineString(idx.iter().map(|&i| v[i].clone()).collect())
2111 }
2112 GeometryColumn::Polygon(v) => {
2113 GeometryColumn::Polygon(idx.iter().map(|&i| v[i].clone()).collect())
2114 }
2115 };
2116 let properties = layer
2117 .properties
2118 .iter()
2119 .map(|(name, col)| {
2120 let col = match col {
2121 PropertyColumn::Numeric(v) => {
2122 PropertyColumn::Numeric(idx.iter().map(|&i| v[i]).collect())
2123 }
2124 PropertyColumn::Categorical(v) => {
2125 PropertyColumn::Categorical(idx.iter().map(|&i| v[i].clone()).collect())
2126 }
2127 PropertyColumn::Vector { width, elem, values } => PropertyColumn::Vector {
2128 width: *width,
2129 elem: *elem,
2130 values: idx
2131 .iter()
2132 .flat_map(|&i| values[i * width..(i + 1) * width].iter().copied())
2133 .collect(),
2134 },
2135 };
2136 (name.clone(), col)
2137 })
2138 .collect();
2139
2140 Cow::Owned(ColumnarLayer {
2141 name: layer.name.clone(),
2142 feature_ids: idx.iter().map(|&i| layer.feature_ids[i]).collect(),
2143 start_times: idx.iter().map(|&i| layer.start_times[i]).collect(),
2144 end_times: idx.iter().map(|&i| layer.end_times[i]).collect(),
2145 geometry,
2146 vertex_times: permute_nested(&layer.vertex_times, &idx),
2147 vertex_values: permute_nested(&layer.vertex_values, &idx),
2148 vertex_value_matrix: permute_nested(&layer.vertex_value_matrix, &idx),
2149 triangles: permute_nested(&layer.triangles, &idx),
2150 properties,
2151 })
2152}
2153
2154fn encode_layer_v2_parts(layer: &ColumnarLayer, cfg: &EncoderConfig) -> Result<EncodedLayerV2> {
2165 layer.validate()?;
2170 let sorted = sort_rows_by_start_time(layer);
2171 let parts = build_layer_parts(&sorted, cfg)?;
2172
2173 let mut qa: BTreeMap<String, (f64, f64)> = BTreeMap::new();
2179 let mut props_fields: Vec<Arc<Field>> = Vec::with_capacity(
2180 parts.fields.len() - parts.reserved_len,
2181 );
2182 for field in &parts.fields[parts.reserved_len..] {
2183 let mut meta = field.metadata().clone();
2184 if let Some(json) = meta.remove(STT_QUANT_ATTR_META_KEY) {
2185 let affine = AttrQuant::from_json(&json).ok_or_else(|| {
2186 Error::Other(format!(
2187 "property '{}' carries an unparseable {STT_QUANT_ATTR_META_KEY} affine",
2188 field.name()
2189 ))
2190 })?;
2191 qa.insert(field.name().clone(), (affine.o, affine.s));
2192 props_fields.push(Arc::new(field.as_ref().clone().with_metadata(meta)));
2193 } else {
2194 props_fields.push(field.clone());
2195 }
2196 }
2197
2198 let tile_meta = TileMeta {
2199 qa: (!qa.is_empty()).then_some(qa),
2200 sorted: Some(true),
2201 t0: parts.min_start_time,
2202 vb: parts.vertex_value_buckets,
2203 vt: parts.vertex_time_encoding,
2204 };
2205 let tile_meta_json = serde_json::to_string(&tile_meta)
2206 .map_err(|e| Error::Other(format!("TILE_META encode failed: {e}")))?;
2207
2208 let mut core_meta: BTreeMap<String, String> = BTreeMap::new();
2212 core_meta.insert("stt:layer".to_string(), parts.layer_name.clone());
2213 core_meta.insert("stt:geometry".to_string(), parts.geometry_name.to_string());
2214 if parts.has_triangles {
2215 core_meta.insert(TRIANGLES_METADATA_KEY.to_string(), "true".to_string());
2216 }
2217 let core_fields: Vec<Arc<Field>> = parts.fields[..parts.reserved_len].to_vec();
2218 let core_columns: Vec<ArrayRef> = parts.columns[..parts.reserved_len].to_vec();
2219 let core_schema = Arc::new(
2220 Schema::new(core_fields).with_metadata(core_meta.into_iter().collect()),
2221 );
2222 let core_ipc = write_ipc_stream(core_schema, core_columns)?;
2223 let core_boundary = split_ipc_at_schema(&core_ipc)?;
2224 let core_tail = core_ipc[core_boundary..].to_vec();
2225 let mut core_template = core_ipc;
2226 core_template.truncate(core_boundary);
2227
2228 let props = if props_fields.is_empty() {
2231 None
2232 } else {
2233 let props_columns: Vec<ArrayRef> = parts.columns[parts.reserved_len..].to_vec();
2234 let props_schema = Arc::new(Schema::new(props_fields));
2235 let props_ipc = write_ipc_stream(props_schema, props_columns)?;
2236 let boundary = split_ipc_at_schema(&props_ipc)?;
2237 let tail = props_ipc[boundary..].to_vec();
2238 let mut template = props_ipc;
2239 template.truncate(boundary);
2240 Some((template, tail))
2241 };
2242
2243 Ok(EncodedLayerV2 {
2244 core_template,
2245 core_tail,
2246 props,
2247 tile_meta_json,
2248 })
2249}
2250
2251pub fn encode_tile(layers: &[ColumnarLayer]) -> Result<Vec<u8>> {
2259 encode_tile_cfg(layers, &EncoderConfig::from_globals())
2260}
2261
2262pub fn encode_tile_quantized(layers: &[ColumnarLayer], quantize_m: Option<f64>) -> Result<Vec<u8>> {
2267 encode_tile_cfg(
2268 layers,
2269 &EncoderConfig {
2270 quantize_coords_m: quantize_m,
2271 ..EncoderConfig::from_globals()
2272 },
2273 )
2274}
2275
2276pub fn encode_tile_with(layers: &[ColumnarLayer], cfg: &EncoderConfig) -> Result<Vec<u8>> {
2281 encode_tile_cfg(layers, cfg)
2282}
2283
2284fn encode_tile_cfg(layers: &[ColumnarLayer], cfg: &EncoderConfig) -> Result<Vec<u8>> {
2289 match cfg.format_version {
2290 FORMAT_VERSION_V1 => encode_tile_frame_v1(layers, cfg),
2291 FORMAT_VERSION_V2 => encode_tile_frame_v2(layers, cfg),
2292 other => Err(Error::Other(format!(
2293 "unsupported format version {other} (this writer emits 1 or 2)"
2294 ))),
2295 }
2296}
2297
2298fn v1_layer_count_ok(count: usize) -> bool {
2305 count < 0x7FFF
2306}
2307
2308fn encode_tile_frame_v1(layers: &[ColumnarLayer], cfg: &EncoderConfig) -> Result<Vec<u8>> {
2310 if !v1_layer_count_ok(layers.len()) {
2314 return Err(Error::Other(format!(
2315 "tile has {} layers, exceeds the {} frame limit",
2316 layers.len(),
2317 ALIGNED_FRAME_FLAG - 2
2318 )));
2319 }
2320 let mut out = Vec::new();
2321 out.extend_from_slice(&(layers.len() as u16 | ALIGNED_FRAME_FLAG).to_le_bytes());
2322 for layer in layers {
2323 let name = layer.name.as_bytes();
2324 if name.len() > u16::MAX as usize {
2325 return Err(Error::Other("layer name too long".into()));
2326 }
2327 let ipc = encode_layer_cfg(layer, cfg)?;
2328 let ipc_len = u32::try_from(ipc.len()).map_err(|_| {
2329 Error::Other(format!(
2330 "layer '{}' IPC stream is {} bytes, exceeding the frame's u32 length field",
2331 layer.name,
2332 ipc.len()
2333 ))
2334 })?;
2335 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2336 out.extend_from_slice(name);
2337 out.extend_from_slice(&ipc_len.to_le_bytes());
2338 let pad = (FRAME_ALIGN - out.len() % FRAME_ALIGN) % FRAME_ALIGN;
2339 out.extend_from_slice(&[0u8; FRAME_ALIGN][..pad]);
2340 out.extend_from_slice(&ipc);
2341 }
2342 Ok(out)
2343}
2344
2345fn pad_to_frame_align(out: &mut Vec<u8>) {
2348 let pad = (FRAME_ALIGN - out.len() % FRAME_ALIGN) % FRAME_ALIGN;
2349 out.extend_from_slice(&[0u8; FRAME_ALIGN][..pad]);
2350}
2351
2352fn encode_tile_frame_v2(layers: &[ColumnarLayer], cfg: &EncoderConfig) -> Result<Vec<u8>> {
2357 if layers.len() > u16::MAX as usize {
2358 return Err(Error::Other(format!(
2359 "tile has {} layers, exceeds the {} frame limit",
2360 layers.len(),
2361 u16::MAX
2362 )));
2363 }
2364 let collector = cfg.template_collector.as_deref();
2365 let mut out = Vec::new();
2366 out.extend_from_slice(&FRAME_V2_ESCAPE.to_le_bytes());
2367 out.push(FRAME_V2_VERSION);
2368 out.push(0); out.extend_from_slice(&(layers.len() as u16).to_le_bytes());
2370 for layer in layers {
2371 let name = layer.name.as_bytes();
2372 if name.len() > u16::MAX as usize {
2373 return Err(Error::Other("layer name too long".into()));
2374 }
2375 let enc = encode_layer_v2_parts(layer, cfg)?;
2376 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
2377 out.extend_from_slice(name);
2378
2379 match collector {
2383 Some(c) => {
2384 out.push(REF_KIND_TEMPLATE_HASH);
2385 out.extend_from_slice(&c.record(&enc.core_template));
2386 }
2387 None => out.push(REF_KIND_INLINE),
2388 }
2389 match (&enc.props, collector) {
2390 (None, _) => out.push(REF_KIND_NO_PROPS),
2391 (Some((template, _)), Some(c)) => {
2392 out.push(REF_KIND_TEMPLATE_HASH);
2393 out.extend_from_slice(&c.record(template));
2394 }
2395 (Some(_), None) => out.push(REF_KIND_INLINE),
2396 }
2397
2398 let mut sections: Vec<(u8, &[u8])> = Vec::new();
2401 if collector.is_none() {
2402 sections.push((SECTION_INLINE_SCHEMA_CORE, &enc.core_template));
2403 }
2404 sections.push((SECTION_TILE_META, enc.tile_meta_json.as_bytes()));
2405 sections.push((SECTION_CORE_BATCH, &enc.core_tail));
2406 if let Some((template, tail)) = &enc.props {
2407 if collector.is_none() {
2408 sections.push((SECTION_INLINE_SCHEMA_PROPS, template));
2409 }
2410 sections.push((SECTION_PROPS_BATCH, tail));
2411 }
2412 out.push(sections.len() as u8);
2413 for (tag, bytes) in §ions {
2414 out.push(*tag);
2415 let len = u32::try_from(bytes.len()).map_err(|_| {
2416 Error::Other(format!(
2417 "layer '{}' section 0x{tag:02x} is {} bytes, exceeding the TOC's u32 \
2418 length field",
2419 layer.name,
2420 bytes.len()
2421 ))
2422 })?;
2423 out.extend_from_slice(&len.to_le_bytes());
2424 }
2425 pad_to_frame_align(&mut out);
2426 for (_, bytes) in §ions {
2427 out.extend_from_slice(bytes);
2428 pad_to_frame_align(&mut out);
2429 }
2430 }
2431 Ok(out)
2432}
2433
2434#[derive(Debug, Clone)]
2440pub struct DecodedLayer {
2441 pub name: String,
2443 pub batch: RecordBatch,
2445}
2446
2447pub fn decode_layer(ipc: &[u8]) -> Result<RecordBatch> {
2449 let reader = StreamReader::try_new(ipc, None)
2450 .map_err(|e| Error::Other(format!("Arrow IPC reader init failed: {e}")))?;
2451 let mut batches: Vec<RecordBatch> = Vec::new();
2452 for batch in reader {
2453 batches.push(batch.map_err(|e| Error::Other(format!("Arrow IPC read failed: {e}")))?);
2454 }
2455 match batches.len() {
2456 0 => Err(Error::Other("tile layer IPC contained no record batch".into())),
2457 1 => Ok(batches.into_iter().next().unwrap()),
2458 _ => arrow::compute::concat_batches(&batches[0].schema(), &batches)
2461 .map_err(|e| Error::Other(format!("failed to concat tile batches: {e}"))),
2462 }
2463}
2464
2465pub fn is_frame_v2(payload: &[u8]) -> bool {
2469 payload.len() >= 2 && u16::from_le_bytes([payload[0], payload[1]]) == FRAME_V2_ESCAPE
2470}
2471
2472pub fn decode_tile(payload: &[u8]) -> Result<Vec<DecodedLayer>> {
2481 if is_frame_v2(payload) {
2482 return decode_tile_v2(payload, None);
2483 }
2484 decode_tile_v1(payload)
2485}
2486
2487pub fn decode_tile_with_templates(
2491 payload: &[u8],
2492 templates: &TemplateRegistry,
2493) -> Result<Vec<DecodedLayer>> {
2494 if is_frame_v2(payload) {
2495 return decode_tile_v2(payload, Some(templates));
2496 }
2497 decode_tile_v1(payload)
2498}
2499
2500fn decode_tile_v1(payload: &[u8]) -> Result<Vec<DecodedLayer>> {
2502 if payload.len() < 2 {
2503 return Err(Error::Other("tile payload too short for layer frame".into()));
2504 }
2505 let raw_count = u16::from_le_bytes([payload[0], payload[1]]);
2506 let aligned = raw_count & ALIGNED_FRAME_FLAG != 0;
2507 let count = (raw_count & !ALIGNED_FRAME_FLAG) as usize;
2508 let mut pos = 2usize;
2509 let mut layers = Vec::with_capacity(count);
2510 for _ in 0..count {
2511 let name_len = read_u16(payload, &mut pos)? as usize;
2512 let name = read_slice(payload, &mut pos, name_len)?;
2513 let name = String::from_utf8(name.to_vec())
2514 .map_err(|e| Error::Other(format!("layer name not utf8: {e}")))?;
2515 let ipc_len = read_u32(payload, &mut pos)? as usize;
2516 if aligned {
2517 let pad = (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
2518 read_slice(payload, &mut pos, pad)?;
2519 }
2520 let ipc = read_slice(payload, &mut pos, ipc_len)?;
2521 let batch = decode_layer(ipc)?;
2522 layers.push(DecodedLayer { name, batch });
2523 }
2524 Ok(layers)
2525}
2526
2527fn splice_decode(template: &[u8], tail: &[u8], what: &str) -> Result<RecordBatch> {
2536 if template.len() < 4 || template[0..4] != [0xFF, 0xFF, 0xFF, 0xFF] {
2537 return Err(Error::Other(format!(
2538 "{what}: schema template does not start with an encapsulated Arrow message"
2539 )));
2540 }
2541 if tail.len() < 4 || tail[0..4] != [0xFF, 0xFF, 0xFF, 0xFF] {
2542 return Err(Error::Other(format!(
2543 "{what}: batch section does not start with the 0xFFFFFFFF continuation marker \
2544 (corrupt or misaligned section)"
2545 )));
2546 }
2547 let mut buf = Vec::with_capacity(template.len() + tail.len());
2548 buf.extend_from_slice(template);
2549 buf.extend_from_slice(tail);
2550 decode_layer(&buf)
2551}
2552
2553fn resolve_template<'a>(
2555 ref_kind: u8,
2556 hash: Option<[u8; 16]>,
2557 inline: Option<&'a [u8]>,
2558 registry: Option<&'a TemplateRegistry>,
2559 what: &str,
2560) -> Result<&'a [u8]> {
2561 match ref_kind {
2562 REF_KIND_INLINE => inline.ok_or_else(|| {
2563 Error::Other(format!("{what}: inline schema section missing from the frame"))
2564 }),
2565 REF_KIND_TEMPLATE_HASH => {
2566 let hash = hash.expect("hash read for ref_kind 1");
2567 let registry = registry.ok_or_else(|| {
2568 Error::Other(format!(
2569 "{what}: frame references schema template {} but no template registry \
2570 was provided — open the dataset through its manifest (or use \
2571 decode_tile_with_templates)",
2572 hex_16(&hash)
2573 ))
2574 })?;
2575 registry.get(&hash).ok_or_else(|| {
2576 Error::Other(format!(
2577 "{what}: schema template {} is not in the dataset's registry \
2578 (manifest.schemas is incomplete or the frame is corrupt)",
2579 hex_16(&hash)
2580 ))
2581 })
2582 }
2583 other => Err(Error::Other(format!(
2584 "{what}: unknown schema ref_kind {other} (this reader knows 0..=2)"
2585 ))),
2586 }
2587}
2588
2589fn hex_16(hash: &[u8; 16]) -> String {
2590 hash.iter().map(|b| format!("{b:02x}")).collect()
2591}
2592
2593fn merge_v2_layer(
2599 core: RecordBatch,
2600 props: Option<RecordBatch>,
2601 meta: &TileMeta,
2602) -> Result<RecordBatch> {
2603 let mut fields: Vec<Arc<Field>> = core.schema().fields().iter().cloned().collect();
2604 let mut columns: Vec<ArrayRef> = core.columns().to_vec();
2605 let mut schema_meta: HashMap<String, String> = core.schema().metadata().clone();
2606
2607 if let Some(props) = props {
2608 if props.num_rows() != core.num_rows() {
2609 return Err(Error::Other(format!(
2610 "tile layer CORE/PROPS row counts disagree: {} vs {}",
2611 core.num_rows(),
2612 props.num_rows()
2613 )));
2614 }
2615 for (field, column) in props.schema().fields().iter().zip(props.columns()) {
2616 let field = match meta.qa.as_ref().and_then(|qa| qa.get(field.name())) {
2620 Some(&(o, s)) => {
2621 let mut m = field.metadata().clone();
2622 m.insert(
2623 STT_QUANT_ATTR_META_KEY.to_string(),
2624 AttrQuant { o, s }.to_json(),
2625 );
2626 Arc::new(field.as_ref().clone().with_metadata(m))
2627 }
2628 None => field.clone(),
2629 };
2630 fields.push(field);
2631 columns.push(column.clone());
2632 }
2633 }
2634
2635 if let Some(t0) = meta.t0 {
2637 schema_meta.insert(TIME_OFFSET_MS_KEY.to_string(), t0.to_string());
2638 }
2639 if let Some((origin, step)) = meta.vt {
2640 schema_meta.insert(VERTEX_TIME_ORIGIN_KEY.to_string(), origin.to_string());
2641 schema_meta.insert(VERTEX_TIME_STEP_KEY.to_string(), step.to_string());
2642 }
2643 if let Some(buckets) = meta.vb {
2644 schema_meta.insert(VERTEX_VALUE_BUCKETS_KEY.to_string(), buckets.to_string());
2645 }
2646
2647 let schema = Arc::new(Schema::new_with_metadata(fields, schema_meta));
2648 RecordBatch::try_new(schema, columns)
2649 .map_err(|e| Error::Other(format!("failed to merge v2 CORE/PROPS batches: {e}")))
2650}
2651
2652fn decode_tile_v2(
2656 payload: &[u8],
2657 registry: Option<&TemplateRegistry>,
2658) -> Result<Vec<DecodedLayer>> {
2659 let mut pos = 0usize;
2660 let escape = read_u16(payload, &mut pos)?;
2661 debug_assert_eq!(escape, FRAME_V2_ESCAPE, "caller dispatched on the escape");
2662 let frame_version = read_slice(payload, &mut pos, 1)?[0];
2663 if frame_version != FRAME_V2_VERSION {
2664 return Err(Error::Other(format!(
2665 "unsupported layer-frame version {frame_version} (this reader knows v2)"
2666 )));
2667 }
2668 let flags = read_slice(payload, &mut pos, 1)?[0];
2669 if flags != 0 {
2670 return Err(Error::Other(format!(
2671 "reserved v2 layer-frame flags must be 0, got {flags:#04x}"
2672 )));
2673 }
2674 let count = read_u16(payload, &mut pos)? as usize;
2675 let mut layers = Vec::with_capacity(count.min(64));
2676 for _ in 0..count {
2677 let name_len = read_u16(payload, &mut pos)? as usize;
2678 let name = read_slice(payload, &mut pos, name_len)?;
2679 let name = String::from_utf8(name.to_vec())
2680 .map_err(|e| Error::Other(format!("layer name not utf8: {e}")))?;
2681
2682 let mut read_ref = |what: &str| -> Result<(u8, Option<[u8; 16]>)> {
2683 let kind = read_slice(payload, &mut pos, 1)?[0];
2684 let hash = if kind == REF_KIND_TEMPLATE_HASH {
2685 let mut h = [0u8; 16];
2686 h.copy_from_slice(read_slice(payload, &mut pos, 16)?);
2687 Some(h)
2688 } else if kind == REF_KIND_INLINE || kind == REF_KIND_NO_PROPS {
2689 None
2690 } else {
2691 return Err(Error::Other(format!(
2692 "layer '{name}' {what}: unknown schema ref_kind {kind} \
2693 (this reader knows 0..=2)"
2694 )));
2695 };
2696 Ok((kind, hash))
2697 };
2698 let (ref_core, core_hash) = read_ref("core")?;
2699 if ref_core == REF_KIND_NO_PROPS {
2700 return Err(Error::Other(format!(
2701 "layer '{name}': ref_kind_core 2 is invalid (every layer has a CORE batch)"
2702 )));
2703 }
2704 let (ref_props, props_hash) = read_ref("props")?;
2705
2706 let section_count = read_slice(payload, &mut pos, 1)?[0] as usize;
2707 let mut toc: Vec<(u8, usize)> = Vec::with_capacity(section_count);
2708 for _ in 0..section_count {
2709 let tag = read_slice(payload, &mut pos, 1)?[0];
2710 let len = read_u32(payload, &mut pos)? as usize;
2711 toc.push((tag, len));
2712 }
2713 let pad = (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
2714 read_slice(payload, &mut pos, pad)?;
2715
2716 let mut sections: BTreeMap<u8, &[u8]> = BTreeMap::new();
2717 for (tag, len) in toc {
2718 let bytes = read_slice(payload, &mut pos, len)?;
2719 let pad = (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
2720 read_slice(payload, &mut pos, pad)?;
2721 if sections.insert(tag, bytes).is_some() {
2722 return Err(Error::Other(format!(
2723 "layer '{name}': duplicate section tag 0x{tag:02x} in the TOC"
2724 )));
2725 }
2726 }
2727
2728 let tile_meta: TileMeta = match sections.get(&SECTION_TILE_META) {
2730 Some(bytes) => serde_json::from_slice(bytes).map_err(|e| {
2731 Error::Other(format!("layer '{name}': TILE_META JSON decode failed: {e}"))
2732 })?,
2733 None => TileMeta::default(),
2734 };
2735
2736 let core_template = resolve_template(
2737 ref_core,
2738 core_hash,
2739 sections.get(&SECTION_INLINE_SCHEMA_CORE).copied(),
2740 registry,
2741 &format!("layer '{name}' core"),
2742 )?;
2743 let core_tail = sections.get(&SECTION_CORE_BATCH).ok_or_else(|| {
2744 Error::Other(format!("layer '{name}': CORE_BATCH section missing"))
2745 })?;
2746 let core = splice_decode(core_template, core_tail, &format!("layer '{name}' core"))?;
2747
2748 let props = if ref_props == REF_KIND_NO_PROPS {
2749 if sections.contains_key(&SECTION_PROPS_BATCH) {
2750 return Err(Error::Other(format!(
2751 "layer '{name}': PROPS_BATCH section present but ref_kind_props \
2752 declares no props"
2753 )));
2754 }
2755 None
2756 } else {
2757 let template = resolve_template(
2758 ref_props,
2759 props_hash,
2760 sections.get(&SECTION_INLINE_SCHEMA_PROPS).copied(),
2761 registry,
2762 &format!("layer '{name}' props"),
2763 )?;
2764 let tail = sections.get(&SECTION_PROPS_BATCH).ok_or_else(|| {
2765 Error::Other(format!("layer '{name}': PROPS_BATCH section missing"))
2766 })?;
2767 Some(splice_decode(template, tail, &format!("layer '{name}' props"))?)
2768 };
2769
2770 let batch = merge_v2_layer(core, props, &tile_meta)?;
2771 layers.push(DecodedLayer { name, batch });
2772 }
2773 Ok(layers)
2774}
2775
2776pub fn frame_v2_template_refs(payload: &[u8]) -> Result<Vec<[u8; 16]>> {
2783 if !is_frame_v2(payload) {
2784 return Err(Error::Other("not a v2 layer frame (missing escape)".into()));
2785 }
2786 let mut pos = 2usize; let frame_version = read_slice(payload, &mut pos, 1)?[0];
2788 if frame_version != FRAME_V2_VERSION {
2789 return Err(Error::Other(format!(
2790 "unsupported layer-frame version {frame_version} (this reader knows v2)"
2791 )));
2792 }
2793 let flags = read_slice(payload, &mut pos, 1)?[0];
2794 if flags != 0 {
2795 return Err(Error::Other(format!(
2796 "reserved v2 layer-frame flags must be 0, got {flags:#04x}"
2797 )));
2798 }
2799 let count = read_u16(payload, &mut pos)? as usize;
2800 let mut refs: Vec<[u8; 16]> = Vec::new();
2801 for _ in 0..count {
2802 let name_len = read_u16(payload, &mut pos)? as usize;
2803 read_slice(payload, &mut pos, name_len)?;
2804 for what in ["core", "props"] {
2805 let kind = read_slice(payload, &mut pos, 1)?[0];
2806 if kind == REF_KIND_TEMPLATE_HASH {
2807 let mut h = [0u8; 16];
2808 h.copy_from_slice(read_slice(payload, &mut pos, 16)?);
2809 refs.push(h);
2810 } else if kind != REF_KIND_INLINE && kind != REF_KIND_NO_PROPS {
2811 return Err(Error::Other(format!(
2812 "{what}: unknown schema ref_kind {kind} (this reader knows 0..=2)"
2813 )));
2814 }
2815 }
2816 let section_count = read_slice(payload, &mut pos, 1)?[0] as usize;
2817 let mut toc: Vec<usize> = Vec::with_capacity(section_count);
2818 for _ in 0..section_count {
2819 read_slice(payload, &mut pos, 1)?; toc.push(read_u32(payload, &mut pos)? as usize);
2821 }
2822 let pad = (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
2823 read_slice(payload, &mut pos, pad)?;
2824 for len in toc {
2825 read_slice(payload, &mut pos, len)?;
2826 let pad = (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
2827 read_slice(payload, &mut pos, pad)?;
2828 }
2829 }
2830 Ok(refs)
2831}
2832
2833fn read_u16(buf: &[u8], pos: &mut usize) -> Result<u16> {
2834 let s = read_slice(buf, pos, 2)?;
2835 Ok(u16::from_le_bytes([s[0], s[1]]))
2836}
2837
2838fn read_u32(buf: &[u8], pos: &mut usize) -> Result<u32> {
2839 let s = read_slice(buf, pos, 4)?;
2840 Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
2841}
2842
2843fn read_slice<'a>(buf: &'a [u8], pos: &mut usize, len: usize) -> Result<&'a [u8]> {
2844 let end = pos
2845 .checked_add(len)
2846 .ok_or_else(|| Error::Other("tile frame length overflow".into()))?;
2847 if end > buf.len() {
2848 return Err(Error::Other("tile frame truncated".into()));
2849 }
2850 let s = &buf[*pos..end];
2851 *pos = end;
2852 Ok(s)
2853}
2854
2855#[cfg(test)]
2856mod tests {
2857 use super::*;
2858
2859 fn sample_point_layer() -> ColumnarLayer {
2860 ColumnarLayer {
2861 name: "points".to_string(),
2862 feature_ids: vec![1, 2, 3],
2863 start_times: vec![1000, 2000, 3000],
2864 end_times: vec![1500, 2500, 3500],
2865 geometry: GeometryColumn::Point(vec![
2866 [-122.4, 37.7],
2867 [-122.5, 37.8],
2868 [-122.6, 37.9],
2869 ]),
2870 vertex_times: None,
2871 vertex_values: None,
2872 triangles: None,
2873 vertex_value_matrix: None,
2874 properties: vec![
2875 (
2876 "speed".to_string(),
2877 PropertyColumn::Numeric(vec![Some(10.0), None, Some(30.0)]),
2878 ),
2879 (
2880 "kind".to_string(),
2881 PropertyColumn::Categorical(vec![
2882 Some("car".to_string()),
2883 Some("bus".to_string()),
2884 None,
2885 ]),
2886 ),
2887 ],
2888 }
2889 }
2890
2891 #[test]
2898 fn encode_tile_with_is_config_driven_not_global() {
2899 let layer = sample_point_layer();
2900 let layers = std::slice::from_ref(&layer);
2901
2902 let plain_cfg = EncoderConfig::default();
2903 let quant_cfg = EncoderConfig {
2904 quantize_coords_m: Some(1.0),
2905 ..EncoderConfig::default()
2906 };
2907 let attr_cfg = EncoderConfig {
2908 quantize_attrs_auto: true,
2909 ..EncoderConfig::default()
2910 };
2911
2912 let plain = encode_tile_with(layers, &plain_cfg).unwrap();
2913 let quant = encode_tile_with(layers, &quant_cfg).unwrap();
2914 let attr = encode_tile_with(layers, &attr_cfg).unwrap();
2915
2916 assert_ne!(plain, quant, "coord quantization must change the tile");
2926 assert_ne!(plain, attr, "attribute quantization must change the tile");
2927 assert_ne!(quant, attr, "the two quantizations differ from each other");
2928
2929 for tile in [&plain, &quant, &attr] {
2932 let rows: usize = decode_tile(tile).unwrap().iter().map(|l| l.batch.num_rows()).sum();
2933 assert_eq!(rows, 3);
2934 }
2935 }
2936
2937 fn sample_line_layer() -> ColumnarLayer {
2938 ColumnarLayer {
2939 name: "tracks".to_string(),
2940 feature_ids: vec![10, 11],
2941 start_times: vec![0, 100],
2942 end_times: vec![50, 200],
2943 geometry: GeometryColumn::LineString(vec![
2944 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
2945 vec![[5.0, 5.0], [6.0, 6.0]],
2946 ]),
2947 vertex_times: Some(vec![vec![0, 25, 50], vec![100, 200]]),
2948 vertex_values: None,
2949 triangles: None,
2950 vertex_value_matrix: None,
2951 properties: vec![],
2952 }
2953 }
2954
2955 fn sample_polygon_layer() -> ColumnarLayer {
2956 ColumnarLayer {
2957 name: "zones".to_string(),
2958 feature_ids: vec![42],
2959 start_times: vec![0],
2960 end_times: vec![1000],
2961 geometry: GeometryColumn::Polygon(vec![vec![
2962 vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]],
2964 vec![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [1.0, 1.0]],
2966 ]]),
2967 vertex_times: None,
2968 vertex_values: None,
2969 triangles: None,
2970 vertex_value_matrix: None,
2971 properties: vec![],
2972 }
2973 }
2974
2975 #[test]
2976 fn categorical_columns_use_dictionary_encoding() {
2977 let layer = ColumnarLayer {
2978 name: "cars".into(),
2979 feature_ids: vec![1, 2, 3, 4, 5],
2980 start_times: vec![0; 5],
2981 end_times: vec![1; 5],
2982 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; 5]),
2983 vertex_times: None,
2984 vertex_values: None,
2985 triangles: None,
2986 vertex_value_matrix: None,
2987 properties: vec![(
2988 "kind".into(),
2989 PropertyColumn::Categorical(vec![
2990 Some("car".into()),
2991 Some("bus".into()),
2992 Some("car".into()),
2993 None,
2994 Some("car".into()),
2995 ]),
2996 )],
2997 };
2998 let ipc = encode_layer(&layer).unwrap();
2999 let batch = decode_layer(&ipc).unwrap();
3000 let field = batch.schema().field_with_name("kind").unwrap().clone();
3001 match field.data_type() {
3002 DataType::Dictionary(k, v) => {
3003 assert_eq!(k.as_ref(), &DataType::UInt16);
3004 assert_eq!(v.as_ref(), &DataType::Utf8);
3005 }
3006 other => panic!("expected Dictionary<UInt16, Utf8>, got {other:?}"),
3007 }
3008
3009 let col = batch
3010 .column_by_name("kind")
3011 .unwrap()
3012 .as_any()
3013 .downcast_ref::<DictionaryArray<UInt16Type>>()
3014 .unwrap();
3015 let values = col
3016 .values()
3017 .as_any()
3018 .downcast_ref::<StringArray>()
3019 .unwrap();
3020 let mut categories: Vec<&str> = (0..values.len()).map(|i| values.value(i)).collect();
3022 categories.sort();
3023 assert_eq!(categories, vec!["bus", "car"]);
3024
3025 assert!(col.is_null(3));
3027 let keys = col.keys();
3028 for i in [0usize, 1, 2, 4] {
3029 assert!(keys.value(i) < values.len() as u16);
3030 }
3031 }
3032
3033 #[test]
3034 fn categorical_overflow_errors_instead_of_corrupting() {
3035 let n = u16::MAX as usize + 1; let kinds: Vec<Option<String>> = (0..n).map(|i| Some(format!("c{i}"))).collect();
3039 let layer = ColumnarLayer {
3040 name: "huge".into(),
3041 feature_ids: (0..n as u64).collect(),
3042 start_times: vec![0; n],
3043 end_times: vec![1; n],
3044 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; n]),
3045 vertex_times: None,
3046 vertex_values: None,
3047 triangles: None,
3048 vertex_value_matrix: None,
3049 properties: vec![("kind".into(), PropertyColumn::Categorical(kinds))],
3050 };
3051 let err = encode_layer(&layer).expect_err("overflowing dictionary must error");
3052 assert!(
3053 err.to_string().contains("distinct values"),
3054 "unexpected error: {err}"
3055 );
3056 }
3057
3058 #[test]
3059 fn geometry_field_advertises_crs_metadata() {
3060 for layer in [sample_point_layer(), sample_line_layer(), sample_polygon_layer()] {
3064 let ipc = encode_layer(&layer).unwrap();
3065 let batch = decode_layer(&ipc).unwrap();
3066 let field = batch.schema().field_with_name("geometry").unwrap().clone();
3067 let meta = field.metadata();
3068 assert_eq!(
3069 meta.get(GEOARROW_EXT_KEY).map(String::as_str),
3070 Some(layer.geometry.geoarrow_name())
3071 );
3072 let crs = meta
3073 .get(GEOARROW_EXT_META_KEY)
3074 .expect("geometry field must carry ARROW:extension:metadata");
3075 assert!(crs.contains("OGC:CRS84"), "crs metadata was: {crs}");
3076 assert!(crs.contains("crs_type"), "crs metadata was: {crs}");
3077 }
3078 }
3079
3080 #[test]
3081 fn point_layer_roundtrips() {
3082 let layer = sample_point_layer();
3083 let ipc = encode_layer(&layer).unwrap();
3084 let batch = decode_layer(&ipc).unwrap();
3085
3086 assert_eq!(batch.num_rows(), 3);
3087 assert_eq!(batch.num_columns(), 6);
3089
3090 let ids = batch
3091 .column_by_name("id")
3092 .unwrap()
3093 .as_any()
3094 .downcast_ref::<UInt64Array>()
3095 .unwrap();
3096 assert_eq!(ids.values(), &[1, 2, 3]);
3097
3098 let geom = batch
3099 .column_by_name("geometry")
3100 .unwrap()
3101 .as_any()
3102 .downcast_ref::<FixedSizeListArray>()
3103 .unwrap();
3104 assert_eq!(geom.len(), 3);
3105 assert_eq!(geom.value_length(), 2);
3106
3107 let geom_field = batch.schema().field_with_name("geometry").unwrap().clone();
3109 assert_eq!(
3110 geom_field.metadata().get(GEOARROW_EXT_KEY).map(String::as_str),
3111 Some("geoarrow.point")
3112 );
3113
3114 let speed = batch
3116 .column_by_name("speed")
3117 .unwrap()
3118 .as_any()
3119 .downcast_ref::<Float64Array>()
3120 .unwrap();
3121 assert!(speed.is_null(1));
3122 assert_eq!(speed.value(0), 10.0);
3123 }
3124
3125 #[test]
3126 fn vector_property_roundtrips_as_fixed_size_list() {
3127 use arrow::array::{Float32Array, UInt8Array};
3131 let layer = ColumnarLayer {
3132 name: "surfels".to_string(),
3133 feature_ids: vec![1, 2],
3134 start_times: vec![0, 10],
3135 end_times: vec![0, 10],
3136 geometry: GeometryColumn::Point(vec![[-122.4, 37.7], [-122.5, 37.8]]),
3137 vertex_times: None,
3138 vertex_values: None,
3139 triangles: None,
3140 vertex_value_matrix: None,
3141 properties: vec![
3142 (
3143 "surfel_quat".to_string(),
3144 PropertyColumn::Vector {
3145 width: 4,
3146 elem: VectorElem::F32,
3147 values: vec![0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5, 0.5],
3148 },
3149 ),
3150 (
3151 "surfel_rgba".to_string(),
3152 PropertyColumn::Vector {
3153 width: 4,
3154 elem: VectorElem::U8,
3155 values: vec![255.0, 0.0, 0.0, 128.0, 0.0, 255.0, 0.0, 255.0],
3156 },
3157 ),
3158 ],
3159 };
3160 let ipc = encode_layer(&layer).unwrap();
3161 let batch = decode_layer(&ipc).unwrap();
3162
3163 let quat = batch
3164 .column_by_name("surfel_quat")
3165 .unwrap()
3166 .as_any()
3167 .downcast_ref::<FixedSizeListArray>()
3168 .unwrap();
3169 assert_eq!(quat.len(), 2);
3170 assert_eq!(quat.value_length(), 4);
3171 let qchild = quat
3172 .values()
3173 .as_any()
3174 .downcast_ref::<Float32Array>()
3175 .unwrap();
3176 assert_eq!(
3177 qchild.values(),
3178 &[0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5, 0.5]
3179 );
3180
3181 let rgba = batch
3182 .column_by_name("surfel_rgba")
3183 .unwrap()
3184 .as_any()
3185 .downcast_ref::<FixedSizeListArray>()
3186 .unwrap();
3187 assert_eq!(rgba.value_length(), 4);
3188 let cchild = rgba
3189 .values()
3190 .as_any()
3191 .downcast_ref::<UInt8Array>()
3192 .unwrap();
3193 assert_eq!(cchild.values(), &[255, 0, 0, 128, 0, 255, 0, 255]);
3194 }
3195
3196 #[test]
3197 fn vector_groups_fuse_scalar_columns_at_encode() {
3198 use arrow::array::Float32Array;
3201 let layer = ColumnarLayer {
3202 name: "surfels".to_string(),
3203 feature_ids: vec![1, 2],
3204 start_times: vec![0, 10],
3205 end_times: vec![0, 10],
3206 geometry: GeometryColumn::Point(vec![[-122.4, 37.7], [-122.5, 37.8]]),
3207 vertex_times: None,
3208 vertex_values: None,
3209 triangles: None,
3210 vertex_value_matrix: None,
3211 properties: vec![
3212 ("qx".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
3213 ("qy".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
3214 ("qz".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
3215 ("qw".into(), PropertyColumn::Numeric(vec![Some(1.0), Some(0.5)])),
3216 ("z".into(), PropertyColumn::Numeric(vec![Some(3.0), Some(4.0)])),
3217 ],
3218 };
3219 let cfg = EncoderConfig {
3223 vector_groups: vec![VectorGroup {
3224 name: "surfel_quat".to_string(),
3225 components: vec!["qx".into(), "qy".into(), "qz".into(), "qw".into()],
3226 elem: VectorElem::F32,
3227 }],
3228 ..EncoderConfig::default()
3229 };
3230 let ipc = encode_layer_with(&layer, &cfg).unwrap();
3231 let batch = decode_layer(&ipc).unwrap();
3232
3233 assert!(batch.column_by_name("qx").is_none());
3235 assert!(batch.column_by_name("z").is_some());
3236 let quat = batch
3237 .column_by_name("surfel_quat")
3238 .unwrap()
3239 .as_any()
3240 .downcast_ref::<FixedSizeListArray>()
3241 .unwrap();
3242 assert_eq!(quat.value_length(), 4);
3243 let qchild = quat
3244 .values()
3245 .as_any()
3246 .downcast_ref::<Float32Array>()
3247 .unwrap();
3248 assert_eq!(qchild.values(), &[0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5, 0.5]);
3249 }
3250
3251 #[test]
3252 fn point_elevation_folds_into_3d_geometry_unquantized() {
3253 use arrow::array::Float64Array;
3254 let layer = ColumnarLayer {
3255 name: "cloud".into(),
3256 feature_ids: vec![1, 2],
3257 start_times: vec![0, 0],
3258 end_times: vec![0, 0],
3259 geometry: GeometryColumn::Point(vec![[-122.4, 37.7], [-122.5, 37.8]]),
3260 vertex_times: None,
3261 vertex_values: None,
3262 triangles: None,
3263 vertex_value_matrix: None,
3264 properties: vec![
3265 ("z".into(), PropertyColumn::Numeric(vec![Some(3.5), Some(9.0)])),
3266 ("speed".into(), PropertyColumn::Numeric(vec![Some(1.0), Some(2.0)])),
3267 ],
3268 };
3269 let cfg = EncoderConfig {
3270 point_elevation_column: "z".to_string(),
3271 ..EncoderConfig::default()
3272 };
3273 let ipc = encode_layer_with(&layer, &cfg).unwrap();
3274 let batch = decode_layer(&ipc).unwrap();
3275
3276 let geom = batch
3278 .column_by_name("geometry")
3279 .unwrap()
3280 .as_any()
3281 .downcast_ref::<FixedSizeListArray>()
3282 .unwrap();
3283 assert_eq!(geom.value_length(), 3);
3284 let coords = geom.values().as_any().downcast_ref::<Float64Array>().unwrap();
3285 assert_eq!(coords.value(2), 3.5); assert_eq!(coords.value(5), 9.0); assert!(batch.column_by_name("z").is_none(), "z folded into geometry");
3288 assert!(batch.column_by_name("speed").is_some(), "other props untouched");
3289 }
3290
3291 #[test]
3292 fn point_elevation_3d_geometry_quantizes_with_z_affine() {
3293 use arrow::array::Int32Array;
3294 let layer = ColumnarLayer {
3295 name: "cloud".into(),
3296 feature_ids: vec![1],
3297 start_times: vec![0],
3298 end_times: vec![0],
3299 geometry: GeometryColumn::Point(vec![[-122.4, 37.7]]),
3300 vertex_times: None,
3301 vertex_values: None,
3302 triangles: None,
3303 vertex_value_matrix: None,
3304 properties: vec![("z".into(), PropertyColumn::Numeric(vec![Some(5.0)]))],
3305 };
3306 let cfg = EncoderConfig {
3307 quantize_coords_m: Some(0.05),
3308 point_elevation_column: "z".to_string(),
3309 ..EncoderConfig::default()
3310 };
3311 let ipc = encode_layer_with(&layer, &cfg).unwrap();
3312 let batch = decode_layer(&ipc).unwrap();
3313
3314 let field = batch.schema().field_with_name("geometry").unwrap().clone();
3315 let affine = QuantAffine::from_json(field.metadata().get(STT_QUANT_META_KEY).unwrap()).unwrap();
3316 assert_eq!(affine.z0, Some(0.0));
3317 assert_eq!(affine.sz, Some(0.05));
3318 let geom = batch
3319 .column_by_name("geometry")
3320 .unwrap()
3321 .as_any()
3322 .downcast_ref::<FixedSizeListArray>()
3323 .unwrap();
3324 assert_eq!(geom.value_length(), 3);
3325 let coords = geom.values().as_any().downcast_ref::<Int32Array>().unwrap();
3326 assert_eq!(coords.value(2), 100);
3328 assert_eq!(affine.z0.unwrap() + coords.value(2) as f64 * affine.sz.unwrap(), 5.0);
3329 }
3330
3331 #[test]
3332 fn quantized_point_layer_roundtrips_within_precision() {
3333 let layer = sample_point_layer();
3334 let ipc = encode_layer_quantized(&layer, Some(1.0)).unwrap();
3335 let batch = decode_layer(&ipc).unwrap();
3336
3337 let geom_field = batch.schema().field_with_name("geometry").unwrap().clone();
3339 let affine = QuantAffine::from_json(
3340 geom_field
3341 .metadata()
3342 .get(STT_QUANT_META_KEY)
3343 .expect("quantized tile must carry the affine"),
3344 )
3345 .unwrap();
3346
3347 let geom = batch
3348 .column_by_name("geometry")
3349 .unwrap()
3350 .as_any()
3351 .downcast_ref::<FixedSizeListArray>()
3352 .unwrap();
3353 assert_eq!(geom.value_type(), DataType::Int32);
3354 let coords = geom
3355 .values()
3356 .as_any()
3357 .downcast_ref::<Int32Array>()
3358 .unwrap();
3359
3360 let original = [[-122.4, 37.7], [-122.5, 37.8], [-122.6, 37.9]];
3361 for (i, [lon, lat]) in original.iter().enumerate() {
3362 let rlon = affine.lon(coords.value(i * 2));
3363 let rlat = affine.lat(coords.value(i * 2 + 1));
3364 let dlon_m = (rlon - lon).abs() * M_PER_DEG_LAT * lat.to_radians().cos();
3366 let dlat_m = (rlat - lat).abs() * M_PER_DEG_LAT;
3367 assert!(dlon_m < 1.0, "lon err {dlon_m} m at point {i}");
3368 assert!(dlat_m < 1.0, "lat err {dlat_m} m at point {i}");
3369 }
3370 }
3371
3372 #[test]
3373 fn quantized_numeric_attr_roundtrips_within_precision_and_is_opt_in() {
3374 let zvals: Vec<Option<f64>> =
3378 vec![Some(1.07), Some(-2.4), Some(15.9), None, Some(40.02)];
3379 let make = || ColumnarLayer {
3380 name: "lidar".into(),
3381 feature_ids: vec![1, 2, 3, 4, 5],
3382 start_times: vec![0; 5],
3383 end_times: vec![1; 5],
3384 geometry: GeometryColumn::Point(vec![[-122.4, 37.7]; 5]),
3385 vertex_times: None,
3386 vertex_values: None,
3387 triangles: None,
3388 vertex_value_matrix: None,
3389 properties: vec![("z".into(), PropertyColumn::Numeric(zvals.clone()))],
3390 };
3391
3392 let plain =
3396 decode_layer(&encode_layer_with(&make(), &EncoderConfig::default()).unwrap()).unwrap();
3397 let zf = plain.schema().field_with_name("z").unwrap().clone();
3398 assert_eq!(zf.data_type(), &DataType::Float64);
3399 assert!(zf.metadata().get(STT_QUANT_ATTR_META_KEY).is_none());
3400
3401 let q = encode_layer_with(
3403 &make(),
3404 &EncoderConfig {
3405 quantize_attrs: HashMap::from([("z".to_string(), 0.05f64)]),
3406 ..EncoderConfig::default()
3407 },
3408 )
3409 .unwrap();
3410
3411 let batch = decode_layer(&q).unwrap();
3412 let field = batch.schema().field_with_name("z").unwrap().clone();
3413 assert_eq!(field.data_type(), &DataType::UInt16);
3415 let affine = AttrQuant::from_json(
3416 field
3417 .metadata()
3418 .get(STT_QUANT_ATTR_META_KEY)
3419 .expect("quantized attr must carry the affine"),
3420 )
3421 .unwrap();
3422
3423 let col = batch
3424 .column_by_name("z")
3425 .unwrap()
3426 .as_any()
3427 .downcast_ref::<UInt16Array>()
3428 .unwrap();
3429 for (i, want) in zvals.iter().enumerate() {
3430 match want {
3431 Some(v) => {
3432 assert!(!col.is_null(i), "row {i} should be present");
3433 let got = affine.value(col.value(i) as i64);
3434 assert!((got - v).abs() <= 0.05 / 2.0 + 1e-9, "z[{i}] {got} vs {v}");
3435 }
3436 None => assert!(col.is_null(i), "row {i} should be null"),
3437 }
3438 }
3439 }
3440
3441 #[test]
3442 fn auto_numeric_quantization_is_range_adaptive_and_opt_in() {
3443 let depth: Vec<Option<f64>> = vec![Some(0.0), Some(10.0), Some(123.4), Some(700.0)];
3447 let make = || ColumnarLayer {
3448 name: "q".into(),
3449 feature_ids: vec![1, 2, 3, 4],
3450 start_times: vec![0; 4],
3451 end_times: vec![1; 4],
3452 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; 4]),
3453 vertex_times: None,
3454 vertex_values: None,
3455 triangles: None,
3456 vertex_value_matrix: None,
3457 properties: vec![("depth".into(), PropertyColumn::Numeric(depth.clone()))],
3458 };
3459
3460 let plain =
3464 decode_layer(&encode_layer_with(&make(), &EncoderConfig::default()).unwrap()).unwrap();
3465 assert_eq!(
3466 plain.schema().field_with_name("depth").unwrap().data_type(),
3467 &DataType::Float64
3468 );
3469
3470 let batch = decode_layer(
3472 &encode_layer_with(
3473 &make(),
3474 &EncoderConfig {
3475 quantize_attrs_auto: true,
3476 ..EncoderConfig::default()
3477 },
3478 )
3479 .unwrap(),
3480 )
3481 .unwrap();
3482
3483 let field = batch.schema().field_with_name("depth").unwrap().clone();
3484 assert_eq!(field.data_type(), &DataType::UInt16);
3485 let aff = AttrQuant::from_json(field.metadata().get(STT_QUANT_ATTR_META_KEY).unwrap()).unwrap();
3486 let col = batch.column_by_name("depth").unwrap().as_any().downcast_ref::<UInt16Array>().unwrap();
3487 let tol = (700.0 - 0.0) / u16::MAX as f64 / 2.0 + 1e-9;
3488 for (i, want) in depth.iter().enumerate() {
3489 let got = aff.value(col.value(i) as i64);
3490 assert!((got - want.unwrap()).abs() <= tol, "depth[{i}] {got} vs {want:?}");
3491 }
3492 assert_eq!(col.value(0), 0);
3494 assert_eq!(col.value(3), u16::MAX);
3495 }
3496
3497 #[test]
3498 fn quantization_shrinks_geometry_and_is_opt_in() {
3499 let line: Vec<[f64; 2]> = (0..400)
3502 .map(|k| [-73.95 + k as f64 * 1e-4, 40.75 + k as f64 * 7e-5])
3503 .collect();
3504 let layer = ColumnarLayer {
3505 name: "q".into(),
3506 feature_ids: vec![1],
3507 start_times: vec![0],
3508 end_times: vec![1],
3509 geometry: GeometryColumn::LineString(vec![line]),
3510 vertex_times: None,
3511 vertex_values: None,
3512 triangles: None,
3513 vertex_value_matrix: None,
3514 properties: vec![],
3515 };
3516 let plain = encode_layer_quantized(&layer, None).unwrap();
3517 let quant = encode_layer_quantized(&layer, Some(1.0)).unwrap();
3518
3519 let pb = decode_layer(&plain).unwrap();
3521 let pf = pb.schema().field_with_name("geometry").unwrap().clone();
3522 assert!(pf.metadata().get(STT_QUANT_META_KEY).is_none());
3523
3524 let qb = decode_layer(&quant).unwrap();
3526 let qf = qb.schema().field_with_name("geometry").unwrap().clone();
3527 assert!(qf.metadata().get(STT_QUANT_META_KEY).is_some());
3528
3529 assert!(
3531 quant.len() < plain.len(),
3532 "quantized {} should be smaller than f64 {}",
3533 quant.len(),
3534 plain.len()
3535 );
3536 }
3537
3538 #[test]
3539 fn line_layer_roundtrips_with_vertex_times() {
3540 let layer = sample_line_layer();
3541 let ipc = encode_layer(&layer).unwrap();
3542 let batch = decode_layer(&ipc).unwrap();
3543
3544 assert_eq!(batch.num_rows(), 2);
3545 let geom = batch
3546 .column_by_name("geometry")
3547 .unwrap()
3548 .as_any()
3549 .downcast_ref::<ListArray>()
3550 .unwrap();
3551 assert_eq!(geom.value(0).len(), 3);
3553 assert_eq!(geom.value(1).len(), 2);
3554
3555 let meta = batch.schema().metadata().clone();
3558 let origin: i64 = meta
3559 .get("stt:vertex_time_origin_ms")
3560 .expect("u16 vertex-time layers carry an origin")
3561 .parse()
3562 .unwrap();
3563 let step: u32 = meta
3564 .get("stt:vertex_time_step_ms")
3565 .expect("u16 vertex-time layers carry a step")
3566 .parse()
3567 .unwrap();
3568 assert_eq!(origin, 0);
3569 assert_eq!(step, 1);
3570
3571 let vt = batch
3572 .column_by_name("vertex_time")
3573 .unwrap()
3574 .as_any()
3575 .downcast_ref::<ListArray>()
3576 .unwrap();
3577 assert_eq!(vt.len(), 2);
3578 let first = vt.value(0);
3579 let deltas = first.as_any().downcast_ref::<arrow::array::UInt16Array>().unwrap();
3580 let absolutes: Vec<i64> = deltas
3581 .values()
3582 .iter()
3583 .map(|d| origin + (*d as i64) * step as i64)
3584 .collect();
3585 assert_eq!(absolutes, vec![0, 25, 50]);
3586 }
3587
3588 #[test]
3589 fn line_layer_roundtrips_with_vertex_values() {
3590 let layer = ColumnarLayer {
3593 name: "drift".into(),
3594 feature_ids: vec![1, 2],
3595 start_times: vec![0, 0],
3596 end_times: vec![100, 100],
3597 geometry: GeometryColumn::LineString(vec![
3598 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
3599 vec![[3.0, 3.0], [4.0, 4.0]],
3600 ]),
3601 vertex_times: None,
3602 vertex_values: Some(vec![vec![5.0, f32::NAN, 27.5], vec![12.0, 13.0]]),
3603 triangles: None,
3604 vertex_value_matrix: None,
3605 properties: vec![],
3606 };
3607 let ipc = encode_layer(&layer).unwrap();
3608 let batch = decode_layer(&ipc).unwrap();
3609
3610 let vv = batch
3611 .column_by_name("vertex_value")
3612 .expect("layers with per-vertex values carry a vertex_value column")
3613 .as_any()
3614 .downcast_ref::<ListArray>()
3615 .unwrap();
3616 assert_eq!(vv.len(), 2);
3617 let first = vv.value(0);
3618 let vals = first.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
3619 assert_eq!(vals.value(0), 5.0);
3620 assert!(vals.value(1).is_nan());
3621 assert_eq!(vals.value(2), 27.5);
3622 let second = vv.value(1);
3623 let vals2 = second.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
3624 assert_eq!(vals2.values(), &[12.0, 13.0]);
3625 }
3626
3627 #[test]
3628 fn line_layer_roundtrips_with_vertex_value_matrix() {
3629 let layer = ColumnarLayer {
3634 name: "flows".into(),
3635 feature_ids: vec![1, 2],
3636 start_times: vec![0, 0],
3637 end_times: vec![1800, 1800],
3638 geometry: GeometryColumn::LineString(vec![
3639 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
3640 vec![[3.0, 3.0], [4.0, 4.0]],
3641 ]),
3642 vertex_times: None,
3643 vertex_values: None,
3644 triangles: None,
3645 vertex_value_matrix: Some(vec![
3647 vec![10.0, 11.0, 20.0, 21.0, 30.0, 31.0],
3648 vec![40.0, 41.0, 50.0, 51.0],
3649 ]),
3650 properties: vec![],
3651 };
3652 let ipc = encode_layer(&layer).unwrap();
3653 let batch = decode_layer(&ipc).unwrap();
3654
3655 let vm = batch
3656 .column_by_name("vertex_value_matrix")
3657 .expect("matrix layers carry a vertex_value_matrix column")
3658 .as_any()
3659 .downcast_ref::<ListArray>()
3660 .unwrap();
3661 assert_eq!(vm.len(), 2);
3662 let f0 = vm.value(0);
3663 let f0v = f0.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
3664 assert_eq!(f0v.values(), &[10.0, 11.0, 20.0, 21.0, 30.0, 31.0]);
3665 let f1 = vm.value(1);
3666 let f1v = f1.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
3667 assert_eq!(f1v.values(), &[40.0, 41.0, 50.0, 51.0]);
3668
3669 assert_eq!(
3671 batch.schema().metadata().get("stt:vertex_value_buckets"),
3672 Some(&"2".to_string())
3673 );
3674 }
3675
3676 #[test]
3677 fn vertex_time_falls_back_to_int64_for_wide_spans() {
3678 let layer = ColumnarLayer {
3683 name: "edge".into(),
3684 feature_ids: vec![1],
3685 start_times: vec![0],
3686 end_times: vec![100],
3687 geometry: GeometryColumn::LineString(vec![vec![[0.0, 0.0], [1.0, 1.0]]]),
3688 vertex_times: Some(vec![vec![0, 100_000_000_000]]),
3689 vertex_values: None,
3690 triangles: None,
3691 vertex_value_matrix: None,
3692 properties: vec![],
3693 };
3694 let ipc = encode_layer(&layer).unwrap();
3695 let batch = decode_layer(&ipc).unwrap();
3696 let schema = batch.schema();
3697 let meta = schema.metadata();
3698 assert!(meta.get("stt:vertex_time_origin_ms").is_none());
3699 assert!(meta.get("stt:vertex_time_step_ms").is_none());
3700 let vt = batch
3701 .column_by_name("vertex_time")
3702 .unwrap()
3703 .as_any()
3704 .downcast_ref::<ListArray>()
3705 .unwrap();
3706 let first = vt.value(0);
3707 let absolutes = first
3708 .as_any()
3709 .downcast_ref::<Int64Array>()
3710 .expect("wide spans must keep the exact Int64 shape");
3711 assert_eq!(absolutes.values(), &[0, 100_000_000_000]);
3712 }
3713
3714 #[test]
3715 fn vertex_time_step_ceiling_is_the_u16_vs_int64_threshold() {
3716 let make = |span: i64| ColumnarLayer {
3719 name: "edge".into(),
3720 feature_ids: vec![1],
3721 start_times: vec![0],
3722 end_times: vec![100],
3723 geometry: GeometryColumn::LineString(vec![vec![[0.0, 0.0], [1.0, 1.0]]]),
3724 vertex_times: Some(vec![vec![0, span]]),
3725 vertex_values: None,
3726 triangles: None,
3727 vertex_value_matrix: None,
3728 properties: vec![],
3729 };
3730
3731 let at_ceiling = decode_layer(&encode_layer(&make(65_535_000)).unwrap()).unwrap();
3732 let schema = at_ceiling.schema();
3733 let step: u32 = schema
3734 .metadata()
3735 .get("stt:vertex_time_step_ms")
3736 .expect("span at the ceiling stays u16-delta encoded")
3737 .parse()
3738 .unwrap();
3739 assert_eq!(step, DEFAULT_VERTEX_TIME_MAX_STEP_MS);
3740
3741 let past_ceiling = decode_layer(&encode_layer(&make(65_536_000)).unwrap()).unwrap();
3742 assert!(past_ceiling
3743 .schema()
3744 .metadata()
3745 .get("stt:vertex_time_step_ms")
3746 .is_none());
3747 let vt = past_ceiling
3748 .column_by_name("vertex_time")
3749 .unwrap()
3750 .as_any()
3751 .downcast_ref::<ListArray>()
3752 .unwrap();
3753 let first = vt.value(0);
3754 let absolutes = first.as_any().downcast_ref::<Int64Array>().unwrap();
3755 assert_eq!(absolutes.values(), &[0, 65_536_000]);
3756 }
3757
3758 #[test]
3759 fn polygon_layer_roundtrips_with_rings() {
3760 let layer = sample_polygon_layer();
3761 let ipc = encode_layer(&layer).unwrap();
3762 let batch = decode_layer(&ipc).unwrap();
3763
3764 let geom = batch
3765 .column_by_name("geometry")
3766 .unwrap()
3767 .as_any()
3768 .downcast_ref::<ListArray>()
3769 .unwrap();
3770 assert_eq!(geom.len(), 1);
3771 let rings = geom.value(0);
3773 let rings = rings.as_any().downcast_ref::<ListArray>().unwrap();
3774 assert_eq!(rings.len(), 2);
3775 assert_eq!(rings.value(0).len(), 5); assert_eq!(rings.value(1).len(), 5); }
3778
3779 #[test]
3780 fn multi_layer_tile_frame_roundtrips() {
3781 let layers = vec![sample_line_layer(), sample_point_layer()];
3782 let payload = encode_tile(&layers).unwrap();
3783 let decoded = decode_tile(&payload).unwrap();
3784
3785 assert_eq!(decoded.len(), 2);
3786 assert_eq!(decoded[0].name, "tracks");
3787 assert_eq!(decoded[1].name, "points");
3788 assert_eq!(decoded[0].batch.num_rows(), 2);
3789 assert_eq!(decoded[1].batch.num_rows(), 3);
3790 assert_eq!(
3792 decoded[1]
3793 .batch
3794 .schema()
3795 .metadata()
3796 .get("stt:layer")
3797 .map(String::as_str),
3798 Some("points")
3799 );
3800 }
3801
3802 #[test]
3803 fn tessellate_polygon_emits_two_triangles_for_a_square() {
3804 let ring: Vec<Coord> = vec![
3807 [0.0, 0.0],
3808 [1.0, 0.0],
3809 [1.0, 1.0],
3810 [0.0, 1.0],
3811 [0.0, 0.0],
3812 ];
3813 let tris = tessellate_polygon(&[ring]);
3814 assert_eq!(tris.len(), 6);
3815 for &i in &tris {
3816 assert!(i < 5);
3817 }
3818 }
3819
3820 #[test]
3821 fn tessellate_polygon_handles_a_hole() {
3822 let exterior: Vec<Coord> =
3826 vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]];
3827 let hole: Vec<Coord> =
3828 vec![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [1.0, 1.0]];
3829 let tris = tessellate_polygon(&[exterior, hole]);
3830 assert!(tris.len() >= 6);
3831 assert_eq!(tris.len() % 3, 0);
3832 for &i in &tris {
3833 assert!(i < 10);
3834 }
3835 }
3836
3837 #[test]
3838 fn tessellate_polygon_handles_degenerate_input() {
3839 assert!(tessellate_polygon(&[]).is_empty());
3841 let degenerate: Vec<Coord> = vec![[0.0, 0.0], [1.0, 1.0]];
3843 assert!(tessellate_polygon(&[degenerate]).is_empty());
3844 }
3845
3846 #[test]
3847 fn polygon_layer_with_triangles_roundtrips() {
3848 let exterior: Vec<Coord> =
3849 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
3850 let tris = tessellate_polygon(&[exterior.clone()]);
3851 assert_eq!(tris.len(), 6);
3852 let layer = ColumnarLayer {
3853 name: "zones".into(),
3854 feature_ids: vec![42],
3855 start_times: vec![0],
3856 end_times: vec![1000],
3857 geometry: GeometryColumn::Polygon(vec![vec![exterior]]),
3858 vertex_times: None,
3859 vertex_values: None,
3860 triangles: Some(vec![tris.clone()]),
3861 vertex_value_matrix: None,
3862 properties: vec![],
3863 };
3864 let ipc = encode_layer(&layer).unwrap();
3865 let batch = decode_layer(&ipc).unwrap();
3866
3867 assert_eq!(
3869 batch
3870 .schema()
3871 .metadata()
3872 .get(TRIANGLES_METADATA_KEY)
3873 .map(String::as_str),
3874 Some("true")
3875 );
3876 let col = batch
3879 .column_by_name("triangles")
3880 .expect("triangles column present")
3881 .as_any()
3882 .downcast_ref::<ListArray>()
3883 .expect("triangles is a List");
3884 assert_eq!(col.len(), 1);
3885 let first = col.value(0);
3886 let values: &arrow::array::UInt16Array = first
3887 .as_any()
3888 .downcast_ref::<arrow::array::UInt16Array>()
3889 .expect("triangle values are UInt16 for small feature-local indices");
3890 assert_eq!(
3891 values.values().iter().map(|&v| v as u32).collect::<Vec<_>>(),
3892 tris
3893 );
3894 }
3895
3896 #[test]
3897 fn polygon_layer_with_oversized_triangle_index_falls_back_to_uint32() {
3898 let exterior: Vec<Coord> =
3902 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
3903 let big_tris = vec![0u32, 1, 70_000];
3904 let layer = ColumnarLayer {
3905 name: "zones".into(),
3906 feature_ids: vec![42],
3907 start_times: vec![0],
3908 end_times: vec![1000],
3909 geometry: GeometryColumn::Polygon(vec![vec![exterior]]),
3910 vertex_times: None,
3911 vertex_values: None,
3912 triangles: Some(vec![big_tris.clone()]),
3913 vertex_value_matrix: None,
3914 properties: vec![],
3915 };
3916 let ipc = encode_layer(&layer).unwrap();
3917 let batch = decode_layer(&ipc).unwrap();
3918
3919 let col = batch
3920 .column_by_name("triangles")
3921 .expect("triangles column present")
3922 .as_any()
3923 .downcast_ref::<ListArray>()
3924 .expect("triangles is a List");
3925 let first = col.value(0);
3926 let values: &arrow::array::UInt32Array = first
3927 .as_any()
3928 .downcast_ref::<arrow::array::UInt32Array>()
3929 .expect("triangle values fall back to UInt32 when an index exceeds u16::MAX");
3930 assert_eq!(values.values().to_vec(), big_tris);
3931 }
3932
3933 #[test]
3934 fn polygon_layer_without_triangles_skips_the_metadata_key() {
3935 let layer = sample_polygon_layer();
3939 let ipc = encode_layer(&layer).unwrap();
3940 let batch = decode_layer(&ipc).unwrap();
3941 assert!(!batch.schema().metadata().contains_key(TRIANGLES_METADATA_KEY));
3942 assert!(batch.column_by_name("triangles").is_none());
3943 }
3944
3945 #[test]
3946 fn non_polygon_layer_drops_stray_triangles() {
3947 let mut layer = sample_point_layer();
3951 layer.triangles = Some(vec![vec![0, 1, 2]; layer.feature_ids.len()]);
3953 let ipc = encode_layer(&layer).unwrap();
3954 let batch = decode_layer(&ipc).unwrap();
3955 assert!(!batch.schema().metadata().contains_key(TRIANGLES_METADATA_KEY));
3956 assert!(batch.column_by_name("triangles").is_none());
3957 }
3958
3959 fn ipc_offsets(payload: &[u8]) -> Vec<(usize, usize)> {
3962 let raw = u16::from_le_bytes([payload[0], payload[1]]);
3963 let aligned = raw & ALIGNED_FRAME_FLAG != 0;
3964 let count = (raw & !ALIGNED_FRAME_FLAG) as usize;
3965 let mut pos = 2usize;
3966 let mut out = Vec::new();
3967 for _ in 0..count {
3968 let name_len =
3969 u16::from_le_bytes([payload[pos], payload[pos + 1]]) as usize;
3970 pos += 2 + name_len;
3971 let ipc_len = u32::from_le_bytes([
3972 payload[pos],
3973 payload[pos + 1],
3974 payload[pos + 2],
3975 payload[pos + 3],
3976 ]) as usize;
3977 pos += 4;
3978 if aligned {
3979 pos += (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
3980 }
3981 out.push((pos, ipc_len));
3982 pos += ipc_len;
3983 }
3984 out
3985 }
3986
3987 #[test]
3988 fn encoded_frames_align_every_ipc_stream_to_8_bytes() {
3989 let mut a = sample_line_layer();
3993 a.name = "x".into();
3994 let mut b = sample_point_layer();
3995 b.name = "a-longer-layer-name".into();
3996 let payload = encode_tile(&[a, b]).unwrap();
3997
3998 let raw = u16::from_le_bytes([payload[0], payload[1]]);
3999 assert_ne!(raw & ALIGNED_FRAME_FLAG, 0, "writer must set the aligned flag");
4000
4001 let offsets = ipc_offsets(&payload);
4002 assert_eq!(offsets.len(), 2);
4003 for (off, _) in &offsets {
4004 assert_eq!(off % 8, 0, "IPC stream at offset {off} is misaligned");
4005 }
4006
4007 let decoded = decode_tile(&payload).unwrap();
4009 assert_eq!(decoded[0].name, "x");
4010 assert_eq!(decoded[1].name, "a-longer-layer-name");
4011 assert_eq!(decoded[0].batch.num_rows(), 2);
4012 assert_eq!(decoded[1].batch.num_rows(), 3);
4013 }
4014
4015 #[test]
4016 fn legacy_unpadded_frames_still_decode() {
4017 let layers = vec![sample_line_layer(), sample_point_layer()];
4021 let aligned_payload = encode_tile(&layers).unwrap();
4022 let aligned = decode_tile(&aligned_payload).unwrap();
4023
4024 let mut legacy: Vec<u8> = Vec::new();
4025 legacy.extend_from_slice(&(layers.len() as u16).to_le_bytes());
4026 for ((off, len), layer) in ipc_offsets(&aligned_payload).iter().zip(&layers) {
4027 let name = layer.name.as_bytes();
4028 legacy.extend_from_slice(&(name.len() as u16).to_le_bytes());
4029 legacy.extend_from_slice(name);
4030 legacy.extend_from_slice(&(*len as u32).to_le_bytes());
4031 legacy.extend_from_slice(&aligned_payload[*off..*off + *len]);
4032 }
4033
4034 let decoded = decode_tile(&legacy).unwrap();
4035 assert_eq!(decoded.len(), aligned.len());
4036 for (l, a) in decoded.iter().zip(&aligned) {
4037 assert_eq!(l.name, a.name);
4038 assert_eq!(l.batch, a.batch);
4039 }
4040 }
4041
4042 #[test]
4043 fn truncated_tile_frame_errors_cleanly() {
4044 let payload = encode_tile(&[sample_point_layer()]).unwrap();
4045 let truncated = &payload[..payload.len() / 2];
4047 assert!(decode_tile(truncated).is_err());
4048 }
4049
4050 #[test]
4051 fn length_mismatch_is_rejected() {
4052 let mut layer = sample_point_layer();
4053 layer.start_times.pop(); assert!(encode_layer(&layer).is_err());
4055 }
4056
4057 #[test]
4063 fn length_mismatch_is_rejected_by_v2_frame_too() {
4064 let mut layer = sample_point_layer();
4065 layer.start_times = vec![3000, 1000, 2000];
4066 layer.end_times.pop(); let err = encode_tile_with(
4068 &[layer],
4069 &EncoderConfig {
4070 format_version: FORMAT_VERSION_V2,
4071 ..EncoderConfig::default()
4072 },
4073 )
4074 .expect_err("length-inconsistent layer must Err through the v2 path");
4075 assert!(err.to_string().contains("end_times"), "got: {err}");
4076 }
4077
4078 #[test]
4084 fn v1_frame_rejects_unrepresentable_layer_counts() {
4085 assert!(v1_layer_count_ok(0));
4087 assert!(v1_layer_count_ok(0x7FFE), "max representable count");
4088 assert!(!v1_layer_count_ok(0x7FFF), "v2-escape collision");
4089 assert!(!v1_layer_count_ok(0x8000), "bit-15 OR is a no-op → count 0");
4090 assert!(!v1_layer_count_ok(0xFFFE), "top of the mangled range");
4091 assert!(!v1_layer_count_ok(0x10000));
4092
4093 let empty = ColumnarLayer {
4097 name: "l".to_string(),
4098 feature_ids: vec![],
4099 start_times: vec![],
4100 end_times: vec![],
4101 geometry: GeometryColumn::Point(vec![]),
4102 vertex_times: None,
4103 vertex_values: None,
4104 triangles: None,
4105 vertex_value_matrix: None,
4106 properties: vec![],
4107 };
4108 let layers = vec![empty; 0x8000];
4109 let err = encode_tile_with(&layers, &EncoderConfig::default())
4110 .expect_err("0x8000 layers must be rejected");
4111 assert!(err.to_string().contains("frame limit"), "got: {err}");
4112 }
4113
4114 #[test]
4115 fn offsets_from_counts_errors_on_i32_overflow() {
4116 let ok = offsets_from_counts([3usize, 2, 0].into_iter()).unwrap();
4119 assert_eq!(ok.len(), 4); let at_limit = offsets_from_counts([i32::MAX as usize].into_iter());
4122 assert!(at_limit.is_ok(), "exactly i32::MAX vertices still fits");
4123
4124 let over = offsets_from_counts([i32::MAX as usize, 1].into_iter())
4125 .expect_err("i32::MAX + 1 total vertices must error");
4126 assert!(over.to_string().contains("32-bit list offsets"), "got: {over}");
4127
4128 assert!(offsets_from_counts([usize::MAX].into_iter()).is_err());
4130 }
4131
4132 #[test]
4133 fn quantize_precision_below_floor_is_rejected() {
4134 let layer = sample_point_layer();
4138 let err = encode_layer_with(
4139 &layer,
4140 &EncoderConfig {
4141 quantize_coords_m: Some(0.001),
4142 ..EncoderConfig::default()
4143 },
4144 )
4145 .expect_err("1 mm precision must be rejected");
4146 assert!(
4147 err.to_string().contains("minimum") && err.to_string().contains("overflow"),
4148 "error must state the minimum: {err}"
4149 );
4150
4151 assert!(0.0187 > MIN_QUANTIZE_COORDS_M);
4152 assert!(encode_layer_with(
4153 &layer,
4154 &EncoderConfig {
4155 quantize_coords_m: Some(0.0187),
4156 ..EncoderConfig::default()
4157 },
4158 )
4159 .is_ok());
4160
4161 assert!(set_quantize_coords_m(0.001).is_err());
4165 assert!(set_quantize_coords_m(0.0).is_ok());
4166 }
4167
4168 #[test]
4169 fn quantized_altitude_outside_i32_errors_instead_of_clamping() {
4170 let make = |z: f64| ColumnarLayer {
4173 name: "cloud".into(),
4174 feature_ids: vec![1],
4175 start_times: vec![0],
4176 end_times: vec![0],
4177 geometry: GeometryColumn::Point(vec![[-122.4, 37.7]]),
4178 vertex_times: None,
4179 vertex_values: None,
4180 triangles: None,
4181 vertex_value_matrix: None,
4182 properties: vec![("z".into(), PropertyColumn::Numeric(vec![Some(z)]))],
4183 };
4184 let cfg = EncoderConfig {
4185 quantize_coords_m: Some(0.05),
4186 point_elevation_column: "z".to_string(),
4187 ..EncoderConfig::default()
4188 };
4189 assert!(encode_layer_with(&make(5.0), &cfg).is_ok());
4191 let err = encode_layer_with(&make(1.0e18), &cfg)
4193 .expect_err("overflowing altitude must error");
4194 let msg = err.to_string();
4195 assert!(
4197 msg.contains("altitude") && msg.contains("1000000000000000000"),
4198 "got: {msg}"
4199 );
4200 }
4201
4202 #[test]
4203 fn quantized_attr_index_beyond_i32_errors_instead_of_clamping() {
4204 let layer = ColumnarLayer {
4207 name: "wide".into(),
4208 feature_ids: vec![1, 2],
4209 start_times: vec![0; 2],
4210 end_times: vec![1; 2],
4211 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; 2]),
4212 vertex_times: None,
4213 vertex_values: None,
4214 triangles: None,
4215 vertex_value_matrix: None,
4216 properties: vec![(
4217 "v".into(),
4218 PropertyColumn::Numeric(vec![Some(0.0), Some(3.0e9)]),
4219 )],
4220 };
4221 let err = encode_layer_with(
4222 &layer,
4223 &EncoderConfig {
4224 quantize_attrs: HashMap::from([("v".to_string(), 1.0f64)]),
4225 ..EncoderConfig::default()
4226 },
4227 )
4228 .expect_err("index 3e9 > i32::MAX must error");
4229 let msg = err.to_string();
4230 assert!(msg.contains("overflows") && msg.contains("3000000000"), "got: {msg}");
4231 }
4232
4233 fn v2_inline(base: &EncoderConfig) -> EncoderConfig {
4240 EncoderConfig {
4241 format_version: FORMAT_VERSION_V2,
4242 template_collector: None,
4243 ..base.clone()
4244 }
4245 }
4246
4247 fn v2_hashed(base: &EncoderConfig, collector: &Arc<TemplateCollector>) -> EncoderConfig {
4250 EncoderConfig {
4251 format_version: FORMAT_VERSION_V2,
4252 template_collector: Some(Arc::clone(collector)),
4253 ..base.clone()
4254 }
4255 }
4256
4257 fn registry_from(collector: &TemplateCollector) -> TemplateRegistry {
4259 let mut registry = TemplateRegistry::new();
4260 for (_, template) in collector.snapshot() {
4261 registry.insert(template);
4262 }
4263 registry
4264 }
4265
4266 fn assert_v2_decodes_like_v1(layers: &[ColumnarLayer], base: &EncoderConfig, what: &str) {
4272 let v1 = decode_tile(&encode_tile_with(layers, base).unwrap()).unwrap();
4273
4274 let inline = decode_tile(&encode_tile_with(layers, &v2_inline(base)).unwrap()).unwrap();
4275 assert_eq!(inline.len(), v1.len(), "{what}: inline layer count");
4276 for (a, b) in inline.iter().zip(&v1) {
4277 assert_eq!(a.name, b.name, "{what}: inline layer name");
4278 assert_eq!(a.batch, b.batch, "{what}: inline v2 decode != v1 decode");
4279 }
4280
4281 let collector = Arc::new(TemplateCollector::new());
4282 let payload = encode_tile_with(layers, &v2_hashed(base, &collector)).unwrap();
4283 let registry = registry_from(&collector);
4284 let hashed = decode_tile_with_templates(&payload, ®istry).unwrap();
4285 assert_eq!(hashed.len(), v1.len(), "{what}: hashed layer count");
4286 for (a, b) in hashed.iter().zip(&v1) {
4287 assert_eq!(a.name, b.name, "{what}: hashed layer name");
4288 assert_eq!(a.batch, b.batch, "{what}: hashed v2 decode != v1 decode");
4289 }
4290 }
4291
4292 #[test]
4298 fn v2_roundtrip_equals_v1_decode_across_payload_shapes() {
4299 let plain = EncoderConfig::default();
4300 let quant = EncoderConfig {
4301 quantize_coords_m: Some(1.0),
4302 quantize_attrs_auto: true,
4303 ..EncoderConfig::default()
4304 };
4305
4306 let two_dicts = ColumnarLayer {
4307 properties: vec![
4308 (
4309 "kind".to_string(),
4310 PropertyColumn::Categorical(vec![
4311 Some("car".into()),
4312 Some("bus".into()),
4313 None,
4314 ]),
4315 ),
4316 (
4317 "color".to_string(),
4318 PropertyColumn::Categorical(vec![
4319 Some("red".into()),
4320 None,
4321 Some("blue".into()),
4322 ]),
4323 ),
4324 ],
4325 ..sample_point_layer()
4326 };
4327
4328 let wide_span_vt = ColumnarLayer {
4329 vertex_times: Some(vec![vec![0, 100_000_000_000], vec![0, 1]]),
4330 ..sample_line_layer()
4331 };
4332
4333 let matrix = ColumnarLayer {
4334 name: "flows".into(),
4335 feature_ids: vec![1, 2],
4336 start_times: vec![0, 0],
4337 end_times: vec![1800, 1800],
4338 geometry: GeometryColumn::LineString(vec![
4339 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
4340 vec![[3.0, 3.0], [4.0, 4.0]],
4341 ]),
4342 vertex_times: None,
4343 vertex_values: None,
4344 triangles: None,
4345 vertex_value_matrix: Some(vec![
4346 vec![10.0, 11.0, 20.0, 21.0, 30.0, 31.0],
4347 vec![40.0, 41.0, 50.0, 51.0],
4348 ]),
4349 properties: vec![],
4350 };
4351
4352 let exterior: Vec<Coord> =
4353 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
4354 let triangles = ColumnarLayer {
4355 triangles: Some(vec![tessellate_polygon(&[exterior.clone()])]),
4356 geometry: GeometryColumn::Polygon(vec![vec![exterior]]),
4357 ..sample_polygon_layer()
4358 };
4359
4360 let empty = ColumnarLayer {
4361 name: "points".into(),
4362 feature_ids: vec![],
4363 start_times: vec![],
4364 end_times: vec![],
4365 geometry: GeometryColumn::Point(vec![]),
4366 vertex_times: None,
4367 vertex_values: None,
4368 triangles: None,
4369 vertex_value_matrix: None,
4370 properties: vec![
4371 ("speed".into(), PropertyColumn::Numeric(vec![])),
4372 ("kind".into(), PropertyColumn::Categorical(vec![])),
4373 ],
4374 };
4375
4376 assert_v2_decodes_like_v1(&[sample_point_layer()], &plain, "points");
4377 assert_v2_decodes_like_v1(&[sample_point_layer()], &quant, "quantized points");
4378 assert_v2_decodes_like_v1(&[two_dicts], &plain, "two dictionary columns");
4379 assert_v2_decodes_like_v1(&[sample_line_layer()], &plain, "u16-delta vertex_time");
4380 assert_v2_decodes_like_v1(&[wide_span_vt], &plain, "exact Int64 vertex_time");
4381 assert_v2_decodes_like_v1(&[matrix], &plain, "vertex-value matrix");
4382 assert_v2_decodes_like_v1(&[triangles], &plain, "pre-tessellated triangles");
4383 assert_v2_decodes_like_v1(&[empty], &quant, "empty-bucket tile");
4384 assert_v2_decodes_like_v1(
4385 &[sample_line_layer(), sample_point_layer()],
4386 &plain,
4387 "multi-layer tile",
4388 );
4389 }
4390
4391 #[test]
4396 fn v2_rows_stable_sorted_by_start_time_after_id_assignment() {
4397 let unsorted = ColumnarLayer {
4398 name: "points".into(),
4399 feature_ids: vec![1, 2, 3, 4],
4400 start_times: vec![3000, 1000, 2000, 1000],
4401 end_times: vec![3500, 1500, 2500, 1600],
4402 geometry: GeometryColumn::Point(vec![
4403 [-122.4, 37.7],
4404 [-122.5, 37.8],
4405 [-122.6, 37.9],
4406 [-122.7, 38.0],
4407 ]),
4408 vertex_times: None,
4409 vertex_values: None,
4410 triangles: None,
4411 vertex_value_matrix: None,
4412 properties: vec![(
4413 "kind".into(),
4414 PropertyColumn::Categorical(vec![
4415 Some("a".into()),
4416 Some("b".into()),
4417 Some("c".into()),
4418 Some("d".into()),
4419 ]),
4420 )],
4421 };
4422
4423 let decoded =
4424 decode_tile(&encode_tile_with(&[unsorted.clone()], &v2_inline(&EncoderConfig::default())).unwrap())
4425 .unwrap();
4426 let batch = &decoded[0].batch;
4427 let starts = batch
4428 .column_by_name("start_time")
4429 .unwrap()
4430 .as_any()
4431 .downcast_ref::<Int64Array>()
4432 .unwrap()
4433 .values()
4434 .to_vec();
4435 assert_eq!(starts, vec![1000, 1000, 2000, 3000]);
4437 let ids = batch
4438 .column_by_name("id")
4439 .unwrap()
4440 .as_any()
4441 .downcast_ref::<UInt64Array>()
4442 .unwrap()
4443 .values()
4444 .to_vec();
4445 assert_eq!(ids, vec![2, 4, 3, 1], "ids must travel with their rows");
4446
4447 let presorted = sort_rows_by_start_time(&unsorted).into_owned();
4449 let v1 = decode_tile(&encode_tile_with(&[presorted], &EncoderConfig::default()).unwrap())
4450 .unwrap();
4451 assert_eq!(batch, &v1[0].batch);
4452 }
4453
4454 #[test]
4460 fn v2_template_constancy_and_type_variant_cardinality() {
4461 let quant = EncoderConfig {
4462 quantize_coords_m: Some(1.0),
4463 quantize_attrs_auto: true,
4464 ..EncoderConfig::default()
4465 };
4466 let collector = Arc::new(TemplateCollector::new());
4467 let cfg = v2_hashed(&quant, &collector);
4468
4469 let tile = |seed: i64, cats: [&str; 2], n: usize| ColumnarLayer {
4470 name: "points".into(),
4471 feature_ids: (0..n as u64).collect(),
4472 start_times: (0..n as i64).map(|i| seed + i * 250).collect(),
4473 end_times: (0..n as i64).map(|i| seed + i * 250 + 100).collect(),
4474 geometry: GeometryColumn::Point(
4475 (0..n).map(|i| [-122.0 + i as f64 * 1e-3, 37.0]).collect(),
4476 ),
4477 vertex_times: None,
4478 vertex_values: None,
4479 triangles: None,
4480 vertex_value_matrix: None,
4481 properties: vec![
4482 (
4483 "speed".into(),
4484 PropertyColumn::Numeric(
4485 (0..n).map(|i| Some(seed as f64 * 0.01 + i as f64)).collect(),
4486 ),
4487 ),
4488 (
4489 "kind".into(),
4490 PropertyColumn::Categorical(
4491 (0..n).map(|i| Some(cats[i % 2].to_string())).collect(),
4492 ),
4493 ),
4494 ],
4495 };
4496
4497 let a = encode_tile_with(&[tile(1_000_000, ["car", "bus"], 3)], &cfg).unwrap();
4498 let b = encode_tile_with(&[tile(9_000_000, ["tram", "ferry"], 5)], &cfg).unwrap();
4499 assert_ne!(a, b, "per-tile content must still differ");
4500 assert_eq!(
4501 collector.len(),
4502 2,
4503 "qa/t0/category/row-count variance must NOT mint new templates (core+props)"
4504 );
4505
4506 let narrow = sample_line_layer();
4509 let wide = ColumnarLayer {
4510 vertex_times: Some(vec![vec![0, 100_000_000_000], vec![0, 1]]),
4511 ..sample_line_layer()
4512 };
4513 let c = encode_tile_with(&[narrow], &cfg).unwrap();
4514 let d = encode_tile_with(&[wide], &cfg).unwrap();
4515 assert_eq!(collector.len(), 4, "u16 vs Int64 vertex_time are distinct templates");
4516
4517 let registry = registry_from(&collector);
4519 for payload in [&a, &b, &c, &d] {
4520 decode_tile_with_templates(payload, ®istry).unwrap();
4521 }
4522 }
4523
4524 #[test]
4527 fn v2_tile_meta_is_canonical_json_and_ignores_unknown_keys() {
4528 let meta = TileMeta {
4529 qa: Some(BTreeMap::from([("speed".to_string(), (0.0, 0.15))])),
4530 sorted: Some(true),
4531 t0: Some(1_577_836_800_000),
4532 vb: Some(24),
4533 vt: Some((1_577_836_800_000, 1000)),
4534 };
4535 assert_eq!(
4536 serde_json::to_string(&meta).unwrap(),
4537 r#"{"qa":{"speed":[0.0,0.15]},"sorted":true,"t0":1577836800000,"vb":24,"vt":[1577836800000,1000]}"#
4538 );
4539 assert_eq!(serde_json::to_string(&TileMeta::default()).unwrap(), "{}");
4541 let parsed: TileMeta =
4543 serde_json::from_str(r#"{"sorted":true,"zz_future":{"x":1}}"#).unwrap();
4544 assert_eq!(parsed.sorted, Some(true));
4545 }
4546
4547 fn v2_section_spans(payload: &[u8]) -> Vec<(u8, usize, usize)> {
4551 assert!(is_frame_v2(payload));
4552 let mut pos = 6usize; let name_len = u16::from_le_bytes([payload[pos], payload[pos + 1]]) as usize;
4554 pos += 2 + name_len;
4555 for _ in 0..2 {
4556 let kind = payload[pos];
4557 pos += 1;
4558 if kind == REF_KIND_TEMPLATE_HASH {
4559 pos += 16;
4560 }
4561 }
4562 let section_count = payload[pos] as usize;
4563 pos += 1;
4564 let mut toc = Vec::with_capacity(section_count);
4565 for _ in 0..section_count {
4566 let tag = payload[pos];
4567 let len = u32::from_le_bytes(payload[pos + 1..pos + 5].try_into().unwrap()) as usize;
4568 toc.push((tag, len));
4569 pos += 5;
4570 }
4571 pos += (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
4572 let mut spans = Vec::with_capacity(section_count);
4573 for (tag, len) in toc {
4574 spans.push((tag, pos, len));
4575 pos += len;
4576 pos += (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
4577 }
4578 spans
4579 }
4580
4581 #[test]
4585 fn v2_stray_zeros_in_batch_section_error_instead_of_empty_tile() {
4586 let payload =
4587 encode_tile_with(&[sample_point_layer()], &v2_inline(&EncoderConfig::default()))
4588 .unwrap();
4589 let (_, off, len) = *v2_section_spans(&payload)
4590 .iter()
4591 .find(|(tag, _, _)| *tag == SECTION_CORE_BATCH)
4592 .expect("CORE_BATCH present");
4593 assert!(len > 4);
4594
4595 let mut doctored = payload.clone();
4596 doctored[off..off + 4].fill(0);
4597 let err = decode_tile(&doctored).expect_err("zeroed continuation must error");
4598 assert!(
4599 err.to_string().contains("0xFFFFFFFF"),
4600 "error must name the continuation guard: {err}"
4601 );
4602
4603 let template = &payload[..4]; assert!(splice_decode(&[0u8; 8], template, "guard").is_err());
4606 }
4607
4608 #[test]
4611 fn v2_truncated_header_and_lying_toc_length_error() {
4612 let payload =
4613 encode_tile_with(&[sample_point_layer()], &v2_inline(&EncoderConfig::default()))
4614 .unwrap();
4615 let first_section_off = v2_section_spans(&payload)[0].1;
4616 for cut in 0..first_section_off {
4617 assert!(decode_tile(&payload[..cut]).is_err(), "cut at {cut} must error");
4618 }
4619
4620 let mut doctored = payload.clone();
4623 let toc0 = first_toc_offset(&payload);
4624 doctored[toc0 + 1..toc0 + 5].copy_from_slice(&u32::MAX.to_le_bytes());
4625 let err = decode_tile(&doctored).expect_err("overrunning TOC length must error");
4626 assert!(err.to_string().contains("truncated"), "got: {err}");
4627 }
4628
4629 fn first_toc_offset(payload: &[u8]) -> usize {
4631 let name_len = u16::from_le_bytes([payload[6], payload[7]]) as usize;
4632 let mut pos = 8 + name_len;
4633 for _ in 0..2 {
4634 let kind = payload[pos];
4635 pos += 1;
4636 if kind == REF_KIND_TEMPLATE_HASH {
4637 pos += 16;
4638 }
4639 }
4640 pos + 1 }
4642
4643 #[test]
4647 fn v2_unknown_section_tag_is_skipped() {
4648 let payload =
4649 encode_tile_with(&[sample_point_layer()], &v2_inline(&EncoderConfig::default()))
4650 .unwrap();
4651 let toc0 = first_toc_offset(&payload);
4652 let mut doctored = payload.clone();
4653 let section_count = doctored[toc0 - 1] as usize;
4655 let mut retagged = false;
4656 for i in 0..section_count {
4657 let at = toc0 + i * 5;
4658 if doctored[at] == SECTION_TILE_META {
4659 doctored[at] = 0x6f; retagged = true;
4661 }
4662 }
4663 assert!(retagged);
4664 let decoded = decode_tile(&doctored).unwrap();
4665 assert_eq!(decoded[0].batch.num_rows(), 3);
4666 assert!(
4667 decoded[0].batch.schema().metadata().get(TIME_OFFSET_MS_KEY).is_none(),
4668 "skipped TILE_META means no t0 re-injection"
4669 );
4670 }
4671
4672 #[test]
4675 fn v2_hash_frame_without_registry_errors_descriptively() {
4676 let collector = Arc::new(TemplateCollector::new());
4677 let payload = encode_tile_with(
4678 &[sample_point_layer()],
4679 &v2_hashed(&EncoderConfig::default(), &collector),
4680 )
4681 .unwrap();
4682
4683 let err = decode_tile(&payload).expect_err("no registry must error");
4684 assert!(
4685 err.to_string().contains("decode_tile_with_templates"),
4686 "error must point at the registry entry point: {err}"
4687 );
4688
4689 let empty = TemplateRegistry::new();
4690 let err = decode_tile_with_templates(&payload, &empty)
4691 .expect_err("incomplete registry must error");
4692 assert!(
4693 err.to_string().contains("not in the dataset's registry"),
4694 "got: {err}"
4695 );
4696 }
4697
4698 #[test]
4704 fn v2_metadata_strip_leaves_template_constant_and_tails_differing() {
4705 let quant = EncoderConfig {
4706 quantize_attrs_auto: true,
4707 ..EncoderConfig::default()
4708 };
4709 let mut early = sample_point_layer();
4710 for t in early.start_times.iter_mut() {
4712 *t += 7_000;
4713 }
4714 let late = sample_point_layer();
4715
4716 let a = encode_layer_v2_parts(&early, &quant).unwrap();
4717 let b = encode_layer_v2_parts(&late, &quant).unwrap();
4718 assert_eq!(a.core_template, b.core_template, "CORE template must be constant");
4719 let (a_props_template, a_props_tail) = a.props.as_ref().unwrap();
4720 let (b_props_template, b_props_tail) = b.props.as_ref().unwrap();
4721 assert_eq!(a_props_template, b_props_template, "PROPS template must be constant");
4722 assert_ne!(a.core_tail, b.core_tail, "t0 shift must land in the tail");
4723 assert_ne!(a.tile_meta_json, b.tile_meta_json, "TILE_META varies per tile");
4724 let _ = (a_props_tail, b_props_tail);
4727 }
4728}