1use crate::error::{Error, Result};
40use crate::types::GeometryType;
41use arrow::array::{
42 Array, ArrayRef, DictionaryArray, FixedSizeListArray, Float32Array, Float32Builder,
43 Float64Array, Int32Array, Int32Builder, Int64Array, Int64Builder, ListArray, ListBuilder,
44 RecordBatch, StringArray, UInt16Array, UInt16Builder, UInt32Builder, UInt64Array, UInt8Array,
45};
46use arrow::buffer::OffsetBuffer;
47use arrow::datatypes::{DataType, Field, Schema, UInt16Type};
48use arrow::ipc::reader::StreamReader;
49use arrow::ipc::writer::StreamWriter;
50use std::collections::{BTreeMap, HashMap, HashSet};
51use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
52use std::sync::{Arc, OnceLock, RwLock};
53
54pub const ALIGNED_FRAME_FLAG: u16 = 0x8000;
59
60const FRAME_ALIGN: usize = 8;
62
63const GEOARROW_EXT_KEY: &str = "ARROW:extension:name";
65
66const GEOARROW_EXT_META_KEY: &str = "ARROW:extension:metadata";
69
70const GEOARROW_CRS_METADATA: &str = r#"{"crs":"OGC:CRS84","crs_type":"authority_code"}"#;
78
79pub type Coord = [f64; 2];
81
82#[derive(Debug, Clone)]
85pub enum GeometryColumn {
86 Point(Vec<Coord>),
88 LineString(Vec<Vec<Coord>>),
90 Polygon(Vec<Vec<Vec<Coord>>>),
92}
93
94impl GeometryColumn {
95 pub fn len(&self) -> usize {
97 match self {
98 GeometryColumn::Point(v) => v.len(),
99 GeometryColumn::LineString(v) => v.len(),
100 GeometryColumn::Polygon(v) => v.len(),
101 }
102 }
103
104 pub fn is_empty(&self) -> bool {
106 self.len() == 0
107 }
108
109 pub fn kind(&self) -> GeometryType {
111 match self {
112 GeometryColumn::Point(_) => GeometryType::Point,
113 GeometryColumn::LineString(_) => GeometryType::LineString,
114 GeometryColumn::Polygon(_) => GeometryType::Polygon,
115 }
116 }
117
118 fn geoarrow_name(&self) -> &'static str {
120 match self {
121 GeometryColumn::Point(_) => "geoarrow.point",
122 GeometryColumn::LineString(_) => "geoarrow.linestring",
123 GeometryColumn::Polygon(_) => "geoarrow.polygon",
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum VectorElem {
132 F32,
134 U8,
136}
137
138#[derive(Debug, Clone)]
140pub enum PropertyColumn {
141 Numeric(Vec<Option<f64>>),
143 Categorical(Vec<Option<String>>),
145 Vector {
154 width: usize,
156 elem: VectorElem,
158 values: Vec<f32>,
161 },
162}
163
164#[derive(Debug, Clone)]
166pub struct ColumnarLayer {
167 pub name: String,
169 pub feature_ids: Vec<u64>,
171 pub start_times: Vec<i64>,
173 pub end_times: Vec<i64>,
175 pub geometry: GeometryColumn,
177 pub vertex_times: Option<Vec<Vec<i64>>>,
180 pub vertex_values: Option<Vec<Vec<f32>>>,
185 pub vertex_value_matrix: Option<Vec<Vec<f32>>>,
195 pub triangles: Option<Vec<Vec<u32>>>,
206 pub properties: Vec<(String, PropertyColumn)>,
208}
209
210impl ColumnarLayer {
211 pub fn feature_count(&self) -> usize {
213 self.feature_ids.len()
214 }
215
216 fn validate(&self) -> Result<()> {
218 let n = self.feature_ids.len();
219 let check = |label: &str, len: usize| -> Result<()> {
220 if len != n {
221 return Err(Error::Other(format!(
222 "tile layer '{}': {} has {} entries, expected {}",
223 self.name, label, len, n
224 )));
225 }
226 Ok(())
227 };
228 check("start_times", self.start_times.len())?;
229 check("end_times", self.end_times.len())?;
230 check("geometry", self.geometry.len())?;
231 if let Some(vt) = &self.vertex_times {
232 check("vertex_times", vt.len())?;
233 }
234 if let Some(vv) = &self.vertex_values {
235 check("vertex_values", vv.len())?;
236 }
237 if let Some(vm) = &self.vertex_value_matrix {
238 check("vertex_value_matrix", vm.len())?;
239 }
240 if let Some(tri) = &self.triangles {
241 check("triangles", tri.len())?;
242 }
243 for (name, col) in &self.properties {
244 match col {
245 PropertyColumn::Numeric(v) => check(&format!("property '{}'", name), v.len())?,
246 PropertyColumn::Categorical(v) => check(&format!("property '{}'", name), v.len())?,
247 PropertyColumn::Vector { width, values, .. } => {
248 if *width == 0 {
249 return Err(Error::Other(format!(
250 "tile layer '{}': vector property '{}' has width 0",
251 self.name, name
252 )));
253 }
254 if values.len() != width * n {
255 return Err(Error::Other(format!(
256 "tile layer '{}': vector property '{}' has {} values, expected {} ({} × {})",
257 self.name,
258 name,
259 values.len(),
260 width * n,
261 width,
262 n
263 )));
264 }
265 }
266 }
267 }
268 Ok(())
269 }
270}
271
272pub const TRIANGLES_METADATA_KEY: &str = "stt:has_triangles";
274
275pub fn tessellate_polygon(rings: &[Vec<Coord>]) -> Vec<u32> {
282 if rings.is_empty() {
283 return Vec::new();
284 }
285 let mut flat: Vec<f64> = Vec::with_capacity(rings.iter().map(|r| r.len()).sum::<usize>() * 2);
287 let mut hole_indices: Vec<usize> = Vec::with_capacity(rings.len().saturating_sub(1));
290 let mut running = 0usize;
291 for (i, ring) in rings.iter().enumerate() {
292 if i > 0 {
293 hole_indices.push(running);
294 }
295 for [x, y] in ring {
296 flat.push(*x);
297 flat.push(*y);
298 }
299 running += ring.len();
300 }
301 if running < 3 {
302 return Vec::new();
303 }
304 match earcutr::earcut(&flat, &hole_indices, 2) {
305 Ok(tris) => tris.into_iter().map(|i| i as u32).collect(),
306 Err(_) => Vec::new(),
307 }
308}
309
310fn offsets_from_counts(counts: impl Iterator<Item = usize>) -> OffsetBuffer<i32> {
316 let mut acc = 0i32;
317 let mut offsets = vec![0i32];
318 for c in counts {
319 acc += c as i32;
320 offsets.push(acc);
321 }
322 OffsetBuffer::new(offsets.into())
323}
324
325const M_PER_DEG_LAT: f64 = 111_320.0;
328
329pub const STT_QUANT_META_KEY: &str = "stt:quant";
333
334#[derive(Debug, Clone, Copy, PartialEq)]
341pub struct QuantAffine {
342 pub x0: f64,
343 pub y0: f64,
344 pub sx: f64,
345 pub sy: f64,
346 pub z0: Option<f64>,
350 pub sz: Option<f64>,
351}
352
353impl QuantAffine {
354 fn to_json(&self) -> String {
355 match (self.z0, self.sz) {
358 (Some(z0), Some(sz)) => format!(
359 r#"{{"x0":{:.17e},"y0":{:.17e},"sx":{:.17e},"sy":{:.17e},"z0":{:.17e},"sz":{:.17e}}}"#,
360 self.x0, self.y0, self.sx, self.sy, z0, sz
361 ),
362 _ => format!(
363 r#"{{"x0":{:.17e},"y0":{:.17e},"sx":{:.17e},"sy":{:.17e}}}"#,
364 self.x0, self.y0, self.sx, self.sy
365 ),
366 }
367 }
368
369 pub fn from_json(s: &str) -> Option<QuantAffine> {
372 let v: serde_json::Value = serde_json::from_str(s).ok()?;
373 let f = |k: &str| v.get(k).and_then(|x| x.as_f64());
374 Some(QuantAffine {
375 x0: f("x0")?,
376 y0: f("y0")?,
377 sx: f("sx")?,
378 sy: f("sy")?,
379 z0: f("z0"),
380 sz: f("sz"),
381 })
382 }
383
384 #[inline]
386 pub fn lon(&self, qx: i32) -> f64 {
387 self.x0 + qx as f64 * self.sx
388 }
389 #[inline]
391 pub fn lat(&self, qy: i32) -> f64 {
392 self.y0 + qy as f64 * self.sy
393 }
394
395 #[inline]
396 fn qx(&self, lon: f64) -> i32 {
397 (((lon - self.x0) / self.sx).round() as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32
398 }
399 #[inline]
400 fn qy(&self, lat: f64) -> i32 {
401 (((lat - self.y0) / self.sy).round() as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32
402 }
403 #[inline]
405 fn qz(&self, z: f64) -> i32 {
406 let z0 = self.z0.unwrap_or(0.0);
407 let sz = self.sz.unwrap_or(1.0);
408 (((z - z0) / sz).round() as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32
409 }
410}
411
412fn world_grid_affine(meters: f64) -> Option<QuantAffine> {
426 if !(meters > 0.0) {
427 return None;
428 }
429 let step = meters / M_PER_DEG_LAT;
430 Some(QuantAffine {
431 x0: -180.0,
432 y0: -90.0,
433 sx: step,
434 sy: step,
435 z0: None,
436 sz: None,
437 })
438}
439
440fn world_grid_affine_3d(meters: f64) -> Option<QuantAffine> {
446 let a = world_grid_affine(meters)?;
447 Some(QuantAffine {
448 z0: Some(0.0),
449 sz: Some(meters),
450 ..a
451 })
452}
453
454fn build_geometry_array_q(
464 geom: &GeometryColumn,
465 quant: Option<&QuantAffine>,
466 point_elev: Option<&[f64]>,
467) -> ArrayRef {
468 if let (GeometryColumn::Point(points), Some(elev)) = (geom, point_elev) {
470 let list_size = 3;
471 let dt = if quant.is_some() { DataType::Int32 } else { DataType::Float64 };
472 let field = Arc::new(Field::new("xyz", dt, false));
473 let child: ArrayRef = match quant {
474 Some(q) => {
475 let mut iv = Vec::with_capacity(points.len() * 3);
476 for (i, [x, y]) in points.iter().enumerate() {
477 iv.push(q.qx(*x));
478 iv.push(q.qy(*y));
479 iv.push(q.qz(elev.get(i).copied().unwrap_or(0.0)));
480 }
481 Arc::new(Int32Array::from(iv))
482 }
483 None => {
484 let mut flat = Vec::with_capacity(points.len() * 3);
485 for (i, [x, y]) in points.iter().enumerate() {
486 flat.push(*x);
487 flat.push(*y);
488 flat.push(elev.get(i).copied().unwrap_or(0.0));
489 }
490 Arc::new(Float64Array::from(flat))
491 }
492 };
493 return Arc::new(FixedSizeListArray::new(field, list_size, child, None));
494 }
495
496 let coord_field = || {
497 let dt = if quant.is_some() {
498 DataType::Int32
499 } else {
500 DataType::Float64
501 };
502 Arc::new(Field::new("xy", dt, false))
503 };
504 let make_leaf = |flat: Vec<f64>| -> ArrayRef {
507 match quant {
508 Some(q) => {
509 let mut iv = Vec::with_capacity(flat.len());
510 let mut i = 0;
511 while i + 1 < flat.len() {
512 iv.push(q.qx(flat[i]));
513 iv.push(q.qy(flat[i + 1]));
514 i += 2;
515 }
516 Arc::new(FixedSizeListArray::new(
517 coord_field(),
518 2,
519 Arc::new(Int32Array::from(iv)),
520 None,
521 ))
522 }
523 None => Arc::new(FixedSizeListArray::new(
524 coord_field(),
525 2,
526 Arc::new(Float64Array::from(flat)),
527 None,
528 )),
529 }
530 };
531
532 match geom {
533 GeometryColumn::Point(points) => {
534 let mut flat = Vec::with_capacity(points.len() * 2);
535 for [x, y] in points {
536 flat.push(*x);
537 flat.push(*y);
538 }
539 make_leaf(flat)
540 }
541 GeometryColumn::LineString(lines) => {
542 let mut flat: Vec<f64> = Vec::new();
543 for line in lines {
544 for [x, y] in line {
545 flat.push(*x);
546 flat.push(*y);
547 }
548 }
549 let coords = make_leaf(flat);
550 let offsets = offsets_from_counts(lines.iter().map(|l| l.len()));
551 let vertex_field = Arc::new(Field::new("vertices", coords.data_type().clone(), false));
552 Arc::new(ListArray::new(vertex_field, offsets, coords, None))
553 }
554 GeometryColumn::Polygon(polys) => {
555 let mut flat: Vec<f64> = Vec::new();
557 let mut ring_sizes: Vec<usize> = Vec::new();
558 let mut rings_per_feature: Vec<usize> = Vec::new();
559 for feature in polys {
560 rings_per_feature.push(feature.len());
561 for ring in feature {
562 ring_sizes.push(ring.len());
563 for [x, y] in ring {
564 flat.push(*x);
565 flat.push(*y);
566 }
567 }
568 }
569 let coords = make_leaf(flat);
570 let ring_offsets = offsets_from_counts(ring_sizes.into_iter());
572 let vertex_field = Arc::new(Field::new("vertices", coords.data_type().clone(), false));
573 let rings: ArrayRef = Arc::new(ListArray::new(
574 vertex_field,
575 ring_offsets,
576 coords,
577 None,
578 ));
579 let feature_offsets = offsets_from_counts(rings_per_feature.into_iter());
581 let ring_field = Arc::new(Field::new("rings", rings.data_type().clone(), false));
582 Arc::new(ListArray::new(ring_field, feature_offsets, rings, None))
583 }
584 }
585}
586
587const VERTEX_TIME_ORIGIN_KEY: &str = "stt:vertex_time_origin_ms";
589const VERTEX_TIME_STEP_KEY: &str = "stt:vertex_time_step_ms";
590const VERTEX_VALUE_BUCKETS_KEY: &str = "stt:vertex_value_buckets";
594const TIME_OFFSET_MS_KEY: &str = "stt:time_offset_ms";
600
601pub const DEFAULT_VERTEX_TIME_MAX_STEP_MS: u32 = 1000;
610
611static VERTEX_TIME_MAX_STEP_MS: AtomicU32 = AtomicU32::new(DEFAULT_VERTEX_TIME_MAX_STEP_MS);
615
616static VERTEX_TIME_FALLBACK_WARNED: AtomicBool = AtomicBool::new(false);
619
620pub fn set_vertex_time_max_step_ms(ms: u32) {
624 VERTEX_TIME_MAX_STEP_MS.store(ms.max(1), Ordering::Relaxed);
625}
626
627pub fn vertex_time_max_step_ms() -> u32 {
629 VERTEX_TIME_MAX_STEP_MS.load(Ordering::Relaxed)
630}
631
632static QUANTIZE_COORDS_UM: AtomicU32 = AtomicU32::new(0);
638
639pub fn set_quantize_coords_m(meters: f64) {
644 let um = if meters > 0.0 {
645 (meters * 1.0e6).round().clamp(1.0, u32::MAX as f64) as u32
646 } else {
647 0
648 };
649 QUANTIZE_COORDS_UM.store(um, Ordering::Relaxed);
650}
651
652pub fn quantize_coords_m() -> Option<f64> {
654 let um = QUANTIZE_COORDS_UM.load(Ordering::Relaxed);
655 (um > 0).then(|| um as f64 / 1.0e6)
656}
657
658pub const STT_QUANT_ATTR_META_KEY: &str = "stt:qa";
664
665#[derive(Debug, Clone, Copy, PartialEq)]
670pub struct AttrQuant {
671 pub o: f64,
672 pub s: f64,
673}
674
675impl AttrQuant {
676 fn to_json(&self) -> String {
677 format!(r#"{{"o":{:.17e},"s":{:.17e}}}"#, self.o, self.s)
679 }
680
681 pub fn from_json(s: &str) -> Option<AttrQuant> {
683 let v: serde_json::Value = serde_json::from_str(s).ok()?;
684 Some(AttrQuant {
685 o: v.get("o")?.as_f64()?,
686 s: v.get("s")?.as_f64()?,
687 })
688 }
689
690 #[inline]
692 pub fn value(&self, q: i64) -> f64 {
693 self.o + q as f64 * self.s
694 }
695}
696
697fn quant_attrs_cell() -> &'static RwLock<HashMap<String, f64>> {
703 static A: OnceLock<RwLock<HashMap<String, f64>>> = OnceLock::new();
704 A.get_or_init(|| RwLock::new(HashMap::new()))
705}
706
707pub fn set_quantize_attrs(map: HashMap<String, f64>) {
709 *quant_attrs_cell().write().unwrap() = map;
710}
711
712pub fn quantize_attrs() -> HashMap<String, f64> {
714 quant_attrs_cell().read().unwrap().clone()
715}
716
717
718static QUANTIZE_ATTRS_AUTO: AtomicBool = AtomicBool::new(false);
727
728pub fn set_quantize_attrs_auto(on: bool) {
732 QUANTIZE_ATTRS_AUTO.store(on, Ordering::Relaxed);
733}
734
735pub fn quantize_attrs_auto() -> bool {
737 QUANTIZE_ATTRS_AUTO.load(Ordering::Relaxed)
738}
739
740#[derive(Debug, Clone)]
747pub struct VectorGroup {
748 pub name: String,
750 pub components: Vec<String>,
752 pub elem: VectorElem,
754}
755
756fn vector_groups_cell() -> &'static RwLock<Vec<VectorGroup>> {
760 static A: OnceLock<RwLock<Vec<VectorGroup>>> = OnceLock::new();
761 A.get_or_init(|| RwLock::new(Vec::new()))
762}
763
764pub fn set_vector_groups(groups: Vec<VectorGroup>) {
766 *vector_groups_cell().write().unwrap() = groups;
767}
768
769pub fn vector_groups() -> Vec<VectorGroup> {
771 vector_groups_cell().read().unwrap().clone()
772}
773
774fn point_elevation_column_cell() -> &'static RwLock<String> {
780 static A: OnceLock<RwLock<String>> = OnceLock::new();
781 A.get_or_init(|| RwLock::new(String::new()))
782}
783
784pub fn set_point_elevation_column(name: &str) {
786 *point_elevation_column_cell().write().unwrap() = name.to_string();
787}
788
789pub fn point_elevation_column() -> String {
791 point_elevation_column_cell().read().unwrap().clone()
792}
793
794#[derive(Debug, Clone)]
808pub struct EncoderConfig {
809 pub quantize_coords_m: Option<f64>,
812 pub quantize_attrs: HashMap<String, f64>,
814 pub quantize_attrs_auto: bool,
816 pub vector_groups: Vec<VectorGroup>,
818 pub point_elevation_column: String,
820 pub vertex_time_max_step_ms: u32,
822}
823
824impl Default for EncoderConfig {
825 fn default() -> Self {
826 Self {
827 quantize_coords_m: None,
828 quantize_attrs: HashMap::new(),
829 quantize_attrs_auto: false,
830 vector_groups: Vec::new(),
831 point_elevation_column: String::new(),
832 vertex_time_max_step_ms: DEFAULT_VERTEX_TIME_MAX_STEP_MS,
833 }
834 }
835}
836
837impl EncoderConfig {
838 pub fn from_globals() -> Self {
841 Self {
842 quantize_coords_m: quantize_coords_m(),
843 quantize_attrs: quantize_attrs(),
844 quantize_attrs_auto: quantize_attrs_auto(),
845 vector_groups: vector_groups(),
846 point_elevation_column: point_elevation_column(),
847 vertex_time_max_step_ms: vertex_time_max_step_ms(),
848 }
849 }
850}
851
852fn group_vector_properties(
861 props: &[(String, PropertyColumn)],
862 n: usize,
863 groups: &[VectorGroup],
864) -> Option<Vec<(String, PropertyColumn)>> {
865 if groups.is_empty() {
866 return None;
867 }
868 let numeric: HashMap<&str, &[Option<f64>]> = props
870 .iter()
871 .filter_map(|(k, c)| match c {
872 PropertyColumn::Numeric(v) => Some((k.as_str(), v.as_slice())),
873 _ => None,
874 })
875 .collect();
876
877 let mut out: Vec<(String, PropertyColumn)> = Vec::new();
878 let mut consumed: HashSet<String> = HashSet::new();
879 let mut any = false;
880 for g in groups {
881 if g.components.is_empty()
882 || !g.components.iter().all(|c| numeric.contains_key(c.as_str()))
883 {
884 continue;
885 }
886 let width = g.components.len();
887 let mut values = vec![0f32; width * n];
888 for (ci, cname) in g.components.iter().enumerate() {
889 let col = numeric[cname.as_str()];
890 for i in 0..n {
891 values[i * width + ci] = col[i].map(|x| x as f32).unwrap_or(0.0);
892 }
893 }
894 for c in &g.components {
895 consumed.insert(c.clone());
896 }
897 out.push((
898 g.name.clone(),
899 PropertyColumn::Vector {
900 width,
901 elem: g.elem,
902 values,
903 },
904 ));
905 any = true;
906 }
907 if !any {
908 return None;
909 }
910 for (k, c) in props {
912 if !consumed.contains(k) {
913 out.push((k.clone(), c.clone()));
914 }
915 }
916 Some(out)
917}
918
919fn build_quantized_numeric_auto(values: &[Option<f64>]) -> Option<(ArrayRef, String)> {
928 let mut min = f64::INFINITY;
929 let mut max = f64::NEG_INFINITY;
930 for v in values.iter().flatten() {
931 if v.is_finite() {
932 min = min.min(*v);
933 max = max.max(*v);
934 }
935 }
936 let (o, s) = if min.is_finite() {
940 if max > min {
941 (min, (max - min) / u16::MAX as f64)
942 } else {
943 (min, 1.0)
944 }
945 } else {
946 (0.0, 1.0)
947 };
948 let affine = AttrQuant { o, s };
949 let mut b = UInt16Builder::with_capacity(values.len());
950 for v in values {
951 match v {
952 Some(x) if x.is_finite() => {
953 let q = (((*x - o) / s).round()).clamp(0.0, u16::MAX as f64) as u16;
954 b.append_value(q);
955 }
956 _ => b.append_null(),
957 }
958 }
959 Some((Arc::new(b.finish()), affine.to_json()))
960}
961
962fn build_quantized_numeric(values: &[Option<f64>], prec: f64) -> Option<(ArrayRef, String)> {
970 if !(prec > 0.0) {
971 return None;
972 }
973 let mut min = f64::INFINITY;
974 for v in values.iter().flatten() {
975 if v.is_finite() && *v < min {
976 min = *v;
977 }
978 }
979 if !min.is_finite() {
980 return None; }
982 let affine = AttrQuant { o: min, s: prec };
983 let mut q: Vec<Option<i64>> = Vec::with_capacity(values.len());
984 let mut max_q: i64 = 0;
985 for v in values {
986 match v {
987 Some(x) if x.is_finite() => {
988 let qi = (((*x - affine.o) / affine.s).round() as i64).max(0);
989 if qi > max_q {
990 max_q = qi;
991 }
992 q.push(Some(qi));
993 }
994 _ => q.push(None),
995 }
996 }
997 let array: ArrayRef = if max_q <= u16::MAX as i64 {
998 let mut b = UInt16Builder::with_capacity(q.len());
999 for qi in &q {
1000 match qi {
1001 Some(v) => b.append_value(*v as u16),
1002 None => b.append_null(),
1003 }
1004 }
1005 Arc::new(b.finish())
1006 } else {
1007 let mut b = Int32Builder::with_capacity(q.len());
1008 for qi in &q {
1009 match qi {
1010 Some(v) => b.append_value((*v).clamp(0, i32::MAX as i64) as i32),
1011 None => b.append_null(),
1012 }
1013 }
1014 Arc::new(b.finish())
1015 };
1016 Some((array, affine.to_json()))
1017}
1018
1019fn build_dictionary_indices(
1032 values: &[Option<String>],
1033) -> Result<(Vec<Option<u16>>, Vec<String>)> {
1034 let mut categories: Vec<String> = Vec::new();
1035 let mut lookup: HashMap<String, u16> = HashMap::new();
1036 let mut indices: Vec<Option<u16>> = Vec::with_capacity(values.len());
1037 for v in values {
1038 match v {
1039 Some(s) => {
1040 if let Some(&idx) = lookup.get(s) {
1041 indices.push(Some(idx));
1042 } else if categories.len() < u16::MAX as usize {
1043 let idx = categories.len() as u16;
1044 categories.push(s.clone());
1045 lookup.insert(s.clone(), idx);
1046 indices.push(Some(idx));
1047 } else {
1048 return Err(Error::Other(format!(
1049 "categorical column has more than {} distinct values, which a \
1050 Dictionary<UInt16, Utf8> key cannot address; split the column \
1051 into multiple categorical fields or widen the key type",
1052 u16::MAX
1053 )));
1054 }
1055 }
1056 None => indices.push(None),
1057 }
1058 }
1059 Ok((indices, categories))
1060}
1061
1062struct VertexTimeColumn {
1065 array: ArrayRef,
1066 encoding: Option<(i64, u32)>,
1071}
1072
1073fn build_vertex_time_array(
1083 vertex_times: &Option<Vec<Vec<i64>>>,
1084 feature_count: usize,
1085 max_step_ms: u32,
1086) -> Option<VertexTimeColumn> {
1087 let vt = vertex_times.as_ref()?;
1088
1089 let mut min = i64::MAX;
1093 let mut max = i64::MIN;
1094 let mut any = false;
1095 for times in vt.iter().take(feature_count) {
1096 for &t in times {
1097 if t < min {
1098 min = t;
1099 }
1100 if t > max {
1101 max = t;
1102 }
1103 any = true;
1104 }
1105 }
1106
1107 if any && max >= min {
1108 let span = (max - min) as u64;
1112 let step = if span <= u16::MAX as u64 {
1115 1u64
1116 } else {
1117 ((span + u16::MAX as u64 - 1) / u16::MAX as u64).max(1)
1118 };
1119 if step <= max_step_ms as u64 {
1122 let step = step as u32;
1123 let mut builder = ListBuilder::new(UInt16Builder::new());
1124 for i in 0..feature_count {
1125 match vt.get(i) {
1126 Some(times) if !times.is_empty() => {
1127 for &t in times {
1128 let delta = ((t - min) as u64 / step as u64).min(u16::MAX as u64) as u16;
1133 builder.values().append_value(delta);
1134 }
1135 builder.append(true);
1136 }
1137 _ => builder.append(false),
1138 }
1139 }
1140 return Some(VertexTimeColumn {
1141 array: Arc::new(builder.finish()),
1142 encoding: Some((min, step)),
1143 });
1144 }
1145 if !VERTEX_TIME_FALLBACK_WARNED.swap(true, Ordering::Relaxed) {
1148 tracing::warn!(
1149 "vertex-time span {}ms exceeds u16-delta ceiling (step {}ms > max {}ms); \
1150 falling back to exact Int64 — payload keeps full precision but is ~4x larger",
1151 span,
1152 step,
1153 max_step_ms
1154 );
1155 }
1156 }
1157
1158 let mut builder = ListBuilder::new(Int64Builder::new());
1161 for i in 0..feature_count {
1162 match vt.get(i) {
1163 Some(times) if !times.is_empty() => {
1164 for &t in times {
1165 builder.values().append_value(t);
1166 }
1167 builder.append(true);
1168 }
1169 _ => builder.append(false),
1170 }
1171 }
1172 Some(VertexTimeColumn {
1173 array: Arc::new(builder.finish()),
1174 encoding: None,
1175 })
1176}
1177
1178fn build_vertex_value_array(
1186 vertex_values: &Option<Vec<Vec<f32>>>,
1187 feature_count: usize,
1188) -> Option<ArrayRef> {
1189 let vv = vertex_values.as_ref()?;
1190 let any = vv.iter().take(feature_count).any(|v| !v.is_empty());
1191 if !any {
1192 return None;
1193 }
1194 let mut builder = ListBuilder::new(Float32Builder::new());
1195 for i in 0..feature_count {
1196 match vv.get(i) {
1197 Some(values) if !values.is_empty() => {
1198 for &v in values {
1199 builder.values().append_value(v);
1200 }
1201 builder.append(true);
1202 }
1203 _ => builder.append(false),
1204 }
1205 }
1206 Some(Arc::new(builder.finish()))
1207}
1208
1209fn infer_vertex_value_buckets(matrix: &[Vec<f32>], geometry: &GeometryColumn) -> Option<u32> {
1215 let lines = match geometry {
1216 GeometryColumn::LineString(lines) => lines,
1217 _ => return None,
1218 };
1219 for (i, m) in matrix.iter().enumerate() {
1220 let nv = lines.get(i)?.len();
1221 if !m.is_empty() && nv > 0 && m.len() % nv == 0 {
1222 return Some((m.len() / nv) as u32);
1223 }
1224 }
1225 None
1226}
1227
1228pub fn encode_layer(layer: &ColumnarLayer) -> Result<Vec<u8>> {
1234 encode_layer_cfg(layer, &EncoderConfig::from_globals())
1235}
1236
1237pub fn encode_layer_quantized(layer: &ColumnarLayer, quantize_m: Option<f64>) -> Result<Vec<u8>> {
1250 encode_layer_cfg(
1251 layer,
1252 &EncoderConfig {
1253 quantize_coords_m: quantize_m,
1254 ..EncoderConfig::from_globals()
1255 },
1256 )
1257}
1258
1259pub fn encode_layer_with(layer: &ColumnarLayer, cfg: &EncoderConfig) -> Result<Vec<u8>> {
1263 encode_layer_cfg(layer, cfg)
1264}
1265
1266fn encode_layer_cfg(layer: &ColumnarLayer, cfg: &EncoderConfig) -> Result<Vec<u8>> {
1269 layer.validate()?;
1270 let n = layer.feature_count();
1271
1272 let mut fields: Vec<Arc<Field>> = Vec::new();
1273 let mut columns: Vec<ArrayRef> = Vec::new();
1274
1275 fields.push(Arc::new(Field::new("id", DataType::UInt64, false)));
1276 columns.push(Arc::new(UInt64Array::from(layer.feature_ids.clone())));
1277
1278 fields.push(Arc::new(Field::new("start_time", DataType::Int64, false)));
1279 columns.push(Arc::new(Int64Array::from(layer.start_times.clone())));
1280
1281 fields.push(Arc::new(Field::new("end_time", DataType::Int64, false)));
1282 columns.push(Arc::new(Int64Array::from(layer.end_times.clone())));
1283
1284 let elev_col = cfg.point_elevation_column.clone();
1288 let point_elev: Option<Vec<f64>> = if !elev_col.is_empty()
1289 && matches!(layer.geometry, GeometryColumn::Point(_))
1290 {
1291 layer.properties.iter().find_map(|(name, col)| match col {
1292 PropertyColumn::Numeric(v) if name == &elev_col => {
1293 let n = layer.feature_count();
1294 let mut out = vec![0.0f64; n];
1295 for (i, x) in v.iter().enumerate() {
1296 if let Some(val) = x {
1297 out[i] = *val;
1298 }
1299 }
1300 Some(out)
1301 }
1302 _ => None,
1303 })
1304 } else {
1305 None
1306 };
1307 let elev_consumed = point_elev.is_some();
1308
1309 let quant = cfg.quantize_coords_m.and_then(|m| {
1311 if point_elev.is_some() {
1312 world_grid_affine_3d(m)
1313 } else {
1314 world_grid_affine(m)
1315 }
1316 });
1317 let geom_array =
1318 build_geometry_array_q(&layer.geometry, quant.as_ref(), point_elev.as_deref());
1319 let mut geom_meta = BTreeMap::new();
1327 geom_meta.insert(
1328 GEOARROW_EXT_KEY.to_string(),
1329 layer.geometry.geoarrow_name().to_string(),
1330 );
1331 match &quant {
1332 Some(q) => {
1342 geom_meta.insert(STT_QUANT_META_KEY.to_string(), q.to_json());
1343 }
1344 None => {
1346 geom_meta.insert(
1347 GEOARROW_EXT_META_KEY.to_string(),
1348 GEOARROW_CRS_METADATA.to_string(),
1349 );
1350 }
1351 }
1352 fields.push(Arc::new(
1353 Field::new("geometry", geom_array.data_type().clone(), false)
1354 .with_metadata(geom_meta.into_iter().collect()),
1355 ));
1356 columns.push(geom_array);
1357
1358 let mut vertex_time_encoding: Option<(i64, u32)> = None;
1361 if let Some(vt_col) = build_vertex_time_array(&layer.vertex_times, n, cfg.vertex_time_max_step_ms) {
1362 fields.push(Arc::new(Field::new(
1363 "vertex_time",
1364 vt_col.array.data_type().clone(),
1365 true,
1366 )));
1367 columns.push(vt_col.array);
1368 vertex_time_encoding = vt_col.encoding;
1369 }
1370
1371 if let Some(vv_array) = build_vertex_value_array(&layer.vertex_values, n) {
1374 fields.push(Arc::new(Field::new(
1375 "vertex_value",
1376 vv_array.data_type().clone(),
1377 true,
1378 )));
1379 columns.push(vv_array);
1380 }
1381
1382 let mut vertex_value_buckets: Option<u32> = None;
1387 if let Some(vm_array) = build_vertex_value_array(&layer.vertex_value_matrix, n) {
1388 fields.push(Arc::new(Field::new(
1389 "vertex_value_matrix",
1390 vm_array.data_type().clone(),
1391 true,
1392 )));
1393 columns.push(vm_array);
1394 vertex_value_buckets = layer
1395 .vertex_value_matrix
1396 .as_ref()
1397 .and_then(|vm| infer_vertex_value_buckets(vm, &layer.geometry));
1398 }
1399
1400 let has_triangles = matches!(layer.geometry, GeometryColumn::Polygon(_))
1404 && layer
1405 .triangles
1406 .as_ref()
1407 .map(|t| t.iter().any(|f| !f.is_empty()))
1408 .unwrap_or(false);
1409 if has_triangles {
1410 let tri = layer.triangles.as_ref().unwrap();
1411 let max_index = tri.iter().flatten().copied().max().unwrap_or(0);
1420 let array: ArrayRef = if max_index <= u16::MAX as u32 {
1421 let mut builder = ListBuilder::new(UInt16Builder::new());
1422 for feature in tri {
1423 for &idx in feature {
1424 builder.values().append_value(idx as u16);
1425 }
1426 builder.append(true);
1429 }
1430 Arc::new(builder.finish())
1431 } else {
1432 let mut builder = ListBuilder::new(UInt32Builder::new());
1433 for feature in tri {
1434 for &idx in feature {
1435 builder.values().append_value(idx);
1436 }
1437 builder.append(true);
1438 }
1439 Arc::new(builder.finish())
1440 };
1441 fields.push(Arc::new(Field::new(
1442 "triangles",
1443 array.data_type().clone(),
1444 false,
1445 )));
1446 columns.push(array);
1447 }
1448
1449 let grouped =
1454 group_vector_properties(&layer.properties, layer.feature_count(), &cfg.vector_groups);
1455 let props_iter: &[(String, PropertyColumn)] =
1456 grouped.as_deref().unwrap_or(&layer.properties);
1457 for (name, col) in props_iter {
1458 if elev_consumed && name == &elev_col {
1461 continue;
1462 }
1463 match col {
1464 PropertyColumn::Numeric(values) => {
1465 let quantized = cfg
1472 .quantize_attrs
1473 .get(name)
1474 .copied()
1475 .filter(|p| *p > 0.0)
1476 .and_then(|p| build_quantized_numeric(values, p))
1477 .or_else(|| {
1478 cfg.quantize_attrs_auto
1481 .then(|| build_quantized_numeric_auto(values))
1482 .flatten()
1483 });
1484 match quantized {
1485 Some((array, affine_json)) => {
1486 let mut m = HashMap::new();
1487 m.insert(STT_QUANT_ATTR_META_KEY.to_string(), affine_json);
1488 fields.push(Arc::new(
1489 Field::new(name, array.data_type().clone(), true).with_metadata(m),
1490 ));
1491 columns.push(array);
1492 }
1493 None => {
1494 fields.push(Arc::new(Field::new(name, DataType::Float64, true)));
1495 columns.push(Arc::new(Float64Array::from(values.clone())));
1496 }
1497 }
1498 }
1499 PropertyColumn::Categorical(values) => {
1500 let (indices, categories) = build_dictionary_indices(values)?;
1504 let key_type = DataType::UInt16;
1505 let value_type = DataType::Utf8;
1506 let dict_type = DataType::Dictionary(Box::new(key_type), Box::new(value_type));
1507 fields.push(Arc::new(Field::new(name, dict_type, true)));
1508
1509 let value_array: ArrayRef = Arc::new(StringArray::from(
1510 categories.iter().map(|s| Some(s.as_str())).collect::<Vec<_>>(),
1511 ));
1512 let key_array = UInt16Array::from(indices);
1513 let dict = DictionaryArray::<UInt16Type>::try_new(key_array, value_array)
1514 .map_err(|e| Error::Other(format!("dictionary build failed: {e}")))?;
1515 columns.push(Arc::new(dict));
1516 }
1517 PropertyColumn::Vector { width, elem, values } => {
1518 let (child, child_dt): (ArrayRef, DataType) = match elem {
1524 VectorElem::F32 => (
1525 Arc::new(Float32Array::from(values.clone())),
1526 DataType::Float32,
1527 ),
1528 VectorElem::U8 => {
1529 let bytes: Vec<u8> = values
1530 .iter()
1531 .map(|v| v.round().clamp(0.0, 255.0) as u8)
1532 .collect();
1533 (Arc::new(UInt8Array::from(bytes)), DataType::UInt8)
1534 }
1535 };
1536 let item_field = Arc::new(Field::new("item", child_dt, false));
1537 let fsl = FixedSizeListArray::new(item_field, *width as i32, child, None);
1538 fields.push(Arc::new(Field::new(
1539 name,
1540 fsl.data_type().clone(),
1541 true,
1542 )));
1543 columns.push(Arc::new(fsl));
1544 }
1545 }
1546 }
1547
1548 let mut schema_meta: BTreeMap<String, String> = BTreeMap::new();
1563 schema_meta.insert("stt:layer".to_string(), layer.name.clone());
1564 schema_meta.insert(
1565 "stt:geometry".to_string(),
1566 layer.geometry.geoarrow_name().to_string(),
1567 );
1568 if let Some(min_start) = layer.start_times.iter().copied().min() {
1574 schema_meta.insert(TIME_OFFSET_MS_KEY.to_string(), min_start.to_string());
1575 }
1576 if let Some((origin, step)) = vertex_time_encoding {
1577 schema_meta.insert(VERTEX_TIME_ORIGIN_KEY.to_string(), origin.to_string());
1578 schema_meta.insert(VERTEX_TIME_STEP_KEY.to_string(), step.to_string());
1579 }
1580 if let Some(buckets) = vertex_value_buckets {
1581 schema_meta.insert(VERTEX_VALUE_BUCKETS_KEY.to_string(), buckets.to_string());
1582 }
1583 if has_triangles {
1584 schema_meta.insert(TRIANGLES_METADATA_KEY.to_string(), "true".to_string());
1585 }
1586 let schema = Arc::new(Schema::new(fields).with_metadata(schema_meta.into_iter().collect()));
1587
1588 let batch = RecordBatch::try_new(schema.clone(), columns)
1589 .map_err(|e| Error::Other(format!("failed to build tile RecordBatch: {e}")))?;
1590
1591 let mut buf = Vec::new();
1592 {
1593 let mut writer = StreamWriter::try_new(&mut buf, &schema)
1594 .map_err(|e| Error::Other(format!("Arrow IPC writer init failed: {e}")))?;
1595 writer
1596 .write(&batch)
1597 .map_err(|e| Error::Other(format!("Arrow IPC write failed: {e}")))?;
1598 writer
1599 .finish()
1600 .map_err(|e| Error::Other(format!("Arrow IPC finish failed: {e}")))?;
1601 }
1602 Ok(buf)
1603}
1604
1605pub fn encode_tile(layers: &[ColumnarLayer]) -> Result<Vec<u8>> {
1613 encode_tile_cfg(layers, &EncoderConfig::from_globals())
1614}
1615
1616pub fn encode_tile_quantized(layers: &[ColumnarLayer], quantize_m: Option<f64>) -> Result<Vec<u8>> {
1621 encode_tile_cfg(
1622 layers,
1623 &EncoderConfig {
1624 quantize_coords_m: quantize_m,
1625 ..EncoderConfig::from_globals()
1626 },
1627 )
1628}
1629
1630pub fn encode_tile_with(layers: &[ColumnarLayer], cfg: &EncoderConfig) -> Result<Vec<u8>> {
1635 encode_tile_cfg(layers, cfg)
1636}
1637
1638fn encode_tile_cfg(layers: &[ColumnarLayer], cfg: &EncoderConfig) -> Result<Vec<u8>> {
1641 if layers.len() >= ALIGNED_FRAME_FLAG as usize {
1642 return Err(Error::Other(format!(
1643 "tile has {} layers, exceeds the {} frame limit",
1644 layers.len(),
1645 ALIGNED_FRAME_FLAG - 1
1646 )));
1647 }
1648 let mut out = Vec::new();
1649 out.extend_from_slice(&(layers.len() as u16 | ALIGNED_FRAME_FLAG).to_le_bytes());
1650 for layer in layers {
1651 let name = layer.name.as_bytes();
1652 if name.len() > u16::MAX as usize {
1653 return Err(Error::Other("layer name too long".into()));
1654 }
1655 let ipc = encode_layer_cfg(layer, cfg)?;
1656 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
1657 out.extend_from_slice(name);
1658 out.extend_from_slice(&(ipc.len() as u32).to_le_bytes());
1659 let pad = (FRAME_ALIGN - out.len() % FRAME_ALIGN) % FRAME_ALIGN;
1660 out.extend_from_slice(&[0u8; FRAME_ALIGN][..pad]);
1661 out.extend_from_slice(&ipc);
1662 }
1663 Ok(out)
1664}
1665
1666#[derive(Debug, Clone)]
1672pub struct DecodedLayer {
1673 pub name: String,
1675 pub batch: RecordBatch,
1677}
1678
1679pub fn decode_layer(ipc: &[u8]) -> Result<RecordBatch> {
1681 let reader = StreamReader::try_new(ipc, None)
1682 .map_err(|e| Error::Other(format!("Arrow IPC reader init failed: {e}")))?;
1683 let mut batches: Vec<RecordBatch> = Vec::new();
1684 for batch in reader {
1685 batches.push(batch.map_err(|e| Error::Other(format!("Arrow IPC read failed: {e}")))?);
1686 }
1687 match batches.len() {
1688 0 => Err(Error::Other("tile layer IPC contained no record batch".into())),
1689 1 => Ok(batches.into_iter().next().unwrap()),
1690 _ => arrow::compute::concat_batches(&batches[0].schema(), &batches)
1693 .map_err(|e| Error::Other(format!("failed to concat tile batches: {e}"))),
1694 }
1695}
1696
1697pub fn decode_tile(payload: &[u8]) -> Result<Vec<DecodedLayer>> {
1703 if payload.len() < 2 {
1704 return Err(Error::Other("tile payload too short for layer frame".into()));
1705 }
1706 let raw_count = u16::from_le_bytes([payload[0], payload[1]]);
1707 let aligned = raw_count & ALIGNED_FRAME_FLAG != 0;
1708 let count = (raw_count & !ALIGNED_FRAME_FLAG) as usize;
1709 let mut pos = 2usize;
1710 let mut layers = Vec::with_capacity(count);
1711 for _ in 0..count {
1712 let name_len = read_u16(payload, &mut pos)? as usize;
1713 let name = read_slice(payload, &mut pos, name_len)?;
1714 let name = String::from_utf8(name.to_vec())
1715 .map_err(|e| Error::Other(format!("layer name not utf8: {e}")))?;
1716 let ipc_len = read_u32(payload, &mut pos)? as usize;
1717 if aligned {
1718 let pad = (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
1719 read_slice(payload, &mut pos, pad)?;
1720 }
1721 let ipc = read_slice(payload, &mut pos, ipc_len)?;
1722 let batch = decode_layer(ipc)?;
1723 layers.push(DecodedLayer { name, batch });
1724 }
1725 Ok(layers)
1726}
1727
1728fn read_u16(buf: &[u8], pos: &mut usize) -> Result<u16> {
1729 let s = read_slice(buf, pos, 2)?;
1730 Ok(u16::from_le_bytes([s[0], s[1]]))
1731}
1732
1733fn read_u32(buf: &[u8], pos: &mut usize) -> Result<u32> {
1734 let s = read_slice(buf, pos, 4)?;
1735 Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
1736}
1737
1738fn read_slice<'a>(buf: &'a [u8], pos: &mut usize, len: usize) -> Result<&'a [u8]> {
1739 let end = pos
1740 .checked_add(len)
1741 .ok_or_else(|| Error::Other("tile frame length overflow".into()))?;
1742 if end > buf.len() {
1743 return Err(Error::Other("tile frame truncated".into()));
1744 }
1745 let s = &buf[*pos..end];
1746 *pos = end;
1747 Ok(s)
1748}
1749
1750#[cfg(test)]
1751mod tests {
1752 use super::*;
1753
1754 fn sample_point_layer() -> ColumnarLayer {
1755 ColumnarLayer {
1756 name: "points".to_string(),
1757 feature_ids: vec![1, 2, 3],
1758 start_times: vec![1000, 2000, 3000],
1759 end_times: vec![1500, 2500, 3500],
1760 geometry: GeometryColumn::Point(vec![
1761 [-122.4, 37.7],
1762 [-122.5, 37.8],
1763 [-122.6, 37.9],
1764 ]),
1765 vertex_times: None,
1766 vertex_values: None,
1767 triangles: None,
1768 vertex_value_matrix: None,
1769 properties: vec![
1770 (
1771 "speed".to_string(),
1772 PropertyColumn::Numeric(vec![Some(10.0), None, Some(30.0)]),
1773 ),
1774 (
1775 "kind".to_string(),
1776 PropertyColumn::Categorical(vec![
1777 Some("car".to_string()),
1778 Some("bus".to_string()),
1779 None,
1780 ]),
1781 ),
1782 ],
1783 }
1784 }
1785
1786 #[test]
1793 fn encode_tile_with_is_config_driven_not_global() {
1794 let layer = sample_point_layer();
1795 let layers = std::slice::from_ref(&layer);
1796
1797 let plain_cfg = EncoderConfig::default();
1798 let quant_cfg = EncoderConfig {
1799 quantize_coords_m: Some(1.0),
1800 ..EncoderConfig::default()
1801 };
1802 let attr_cfg = EncoderConfig {
1803 quantize_attrs_auto: true,
1804 ..EncoderConfig::default()
1805 };
1806
1807 let plain = encode_tile_with(layers, &plain_cfg).unwrap();
1808 let quant = encode_tile_with(layers, &quant_cfg).unwrap();
1809 let attr = encode_tile_with(layers, &attr_cfg).unwrap();
1810
1811 assert_ne!(plain, quant, "coord quantization must change the tile");
1821 assert_ne!(plain, attr, "attribute quantization must change the tile");
1822 assert_ne!(quant, attr, "the two quantizations differ from each other");
1823
1824 for tile in [&plain, &quant, &attr] {
1827 let rows: usize = decode_tile(tile).unwrap().iter().map(|l| l.batch.num_rows()).sum();
1828 assert_eq!(rows, 3);
1829 }
1830 }
1831
1832 fn sample_line_layer() -> ColumnarLayer {
1833 ColumnarLayer {
1834 name: "tracks".to_string(),
1835 feature_ids: vec![10, 11],
1836 start_times: vec![0, 100],
1837 end_times: vec![50, 200],
1838 geometry: GeometryColumn::LineString(vec![
1839 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
1840 vec![[5.0, 5.0], [6.0, 6.0]],
1841 ]),
1842 vertex_times: Some(vec![vec![0, 25, 50], vec![100, 200]]),
1843 vertex_values: None,
1844 triangles: None,
1845 vertex_value_matrix: None,
1846 properties: vec![],
1847 }
1848 }
1849
1850 fn sample_polygon_layer() -> ColumnarLayer {
1851 ColumnarLayer {
1852 name: "zones".to_string(),
1853 feature_ids: vec![42],
1854 start_times: vec![0],
1855 end_times: vec![1000],
1856 geometry: GeometryColumn::Polygon(vec![vec![
1857 vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]],
1859 vec![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [1.0, 1.0]],
1861 ]]),
1862 vertex_times: None,
1863 vertex_values: None,
1864 triangles: None,
1865 vertex_value_matrix: None,
1866 properties: vec![],
1867 }
1868 }
1869
1870 #[test]
1871 fn categorical_columns_use_dictionary_encoding() {
1872 let layer = ColumnarLayer {
1873 name: "cars".into(),
1874 feature_ids: vec![1, 2, 3, 4, 5],
1875 start_times: vec![0; 5],
1876 end_times: vec![1; 5],
1877 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; 5]),
1878 vertex_times: None,
1879 vertex_values: None,
1880 triangles: None,
1881 vertex_value_matrix: None,
1882 properties: vec![(
1883 "kind".into(),
1884 PropertyColumn::Categorical(vec![
1885 Some("car".into()),
1886 Some("bus".into()),
1887 Some("car".into()),
1888 None,
1889 Some("car".into()),
1890 ]),
1891 )],
1892 };
1893 let ipc = encode_layer(&layer).unwrap();
1894 let batch = decode_layer(&ipc).unwrap();
1895 let field = batch.schema().field_with_name("kind").unwrap().clone();
1896 match field.data_type() {
1897 DataType::Dictionary(k, v) => {
1898 assert_eq!(k.as_ref(), &DataType::UInt16);
1899 assert_eq!(v.as_ref(), &DataType::Utf8);
1900 }
1901 other => panic!("expected Dictionary<UInt16, Utf8>, got {other:?}"),
1902 }
1903
1904 let col = batch
1905 .column_by_name("kind")
1906 .unwrap()
1907 .as_any()
1908 .downcast_ref::<DictionaryArray<UInt16Type>>()
1909 .unwrap();
1910 let values = col
1911 .values()
1912 .as_any()
1913 .downcast_ref::<StringArray>()
1914 .unwrap();
1915 let mut categories: Vec<&str> = (0..values.len()).map(|i| values.value(i)).collect();
1917 categories.sort();
1918 assert_eq!(categories, vec!["bus", "car"]);
1919
1920 assert!(col.is_null(3));
1922 let keys = col.keys();
1923 for i in [0usize, 1, 2, 4] {
1924 assert!(keys.value(i) < values.len() as u16);
1925 }
1926 }
1927
1928 #[test]
1929 fn categorical_overflow_errors_instead_of_corrupting() {
1930 let n = u16::MAX as usize + 1; let kinds: Vec<Option<String>> = (0..n).map(|i| Some(format!("c{i}"))).collect();
1934 let layer = ColumnarLayer {
1935 name: "huge".into(),
1936 feature_ids: (0..n as u64).collect(),
1937 start_times: vec![0; n],
1938 end_times: vec![1; n],
1939 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; n]),
1940 vertex_times: None,
1941 vertex_values: None,
1942 triangles: None,
1943 vertex_value_matrix: None,
1944 properties: vec![("kind".into(), PropertyColumn::Categorical(kinds))],
1945 };
1946 let err = encode_layer(&layer).expect_err("overflowing dictionary must error");
1947 assert!(
1948 err.to_string().contains("distinct values"),
1949 "unexpected error: {err}"
1950 );
1951 }
1952
1953 #[test]
1954 fn geometry_field_advertises_crs_metadata() {
1955 for layer in [sample_point_layer(), sample_line_layer(), sample_polygon_layer()] {
1959 let ipc = encode_layer(&layer).unwrap();
1960 let batch = decode_layer(&ipc).unwrap();
1961 let field = batch.schema().field_with_name("geometry").unwrap().clone();
1962 let meta = field.metadata();
1963 assert_eq!(
1964 meta.get(GEOARROW_EXT_KEY).map(String::as_str),
1965 Some(layer.geometry.geoarrow_name())
1966 );
1967 let crs = meta
1968 .get(GEOARROW_EXT_META_KEY)
1969 .expect("geometry field must carry ARROW:extension:metadata");
1970 assert!(crs.contains("OGC:CRS84"), "crs metadata was: {crs}");
1971 assert!(crs.contains("crs_type"), "crs metadata was: {crs}");
1972 }
1973 }
1974
1975 #[test]
1976 fn point_layer_roundtrips() {
1977 let layer = sample_point_layer();
1978 let ipc = encode_layer(&layer).unwrap();
1979 let batch = decode_layer(&ipc).unwrap();
1980
1981 assert_eq!(batch.num_rows(), 3);
1982 assert_eq!(batch.num_columns(), 6);
1984
1985 let ids = batch
1986 .column_by_name("id")
1987 .unwrap()
1988 .as_any()
1989 .downcast_ref::<UInt64Array>()
1990 .unwrap();
1991 assert_eq!(ids.values(), &[1, 2, 3]);
1992
1993 let geom = batch
1994 .column_by_name("geometry")
1995 .unwrap()
1996 .as_any()
1997 .downcast_ref::<FixedSizeListArray>()
1998 .unwrap();
1999 assert_eq!(geom.len(), 3);
2000 assert_eq!(geom.value_length(), 2);
2001
2002 let geom_field = batch.schema().field_with_name("geometry").unwrap().clone();
2004 assert_eq!(
2005 geom_field.metadata().get(GEOARROW_EXT_KEY).map(String::as_str),
2006 Some("geoarrow.point")
2007 );
2008
2009 let speed = batch
2011 .column_by_name("speed")
2012 .unwrap()
2013 .as_any()
2014 .downcast_ref::<Float64Array>()
2015 .unwrap();
2016 assert!(speed.is_null(1));
2017 assert_eq!(speed.value(0), 10.0);
2018 }
2019
2020 #[test]
2021 fn vector_property_roundtrips_as_fixed_size_list() {
2022 use arrow::array::{Float32Array, UInt8Array};
2026 let layer = ColumnarLayer {
2027 name: "surfels".to_string(),
2028 feature_ids: vec![1, 2],
2029 start_times: vec![0, 10],
2030 end_times: vec![0, 10],
2031 geometry: GeometryColumn::Point(vec![[-122.4, 37.7], [-122.5, 37.8]]),
2032 vertex_times: None,
2033 vertex_values: None,
2034 triangles: None,
2035 vertex_value_matrix: None,
2036 properties: vec![
2037 (
2038 "surfel_quat".to_string(),
2039 PropertyColumn::Vector {
2040 width: 4,
2041 elem: VectorElem::F32,
2042 values: vec![0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5, 0.5],
2043 },
2044 ),
2045 (
2046 "surfel_rgba".to_string(),
2047 PropertyColumn::Vector {
2048 width: 4,
2049 elem: VectorElem::U8,
2050 values: vec![255.0, 0.0, 0.0, 128.0, 0.0, 255.0, 0.0, 255.0],
2051 },
2052 ),
2053 ],
2054 };
2055 let ipc = encode_layer(&layer).unwrap();
2056 let batch = decode_layer(&ipc).unwrap();
2057
2058 let quat = batch
2059 .column_by_name("surfel_quat")
2060 .unwrap()
2061 .as_any()
2062 .downcast_ref::<FixedSizeListArray>()
2063 .unwrap();
2064 assert_eq!(quat.len(), 2);
2065 assert_eq!(quat.value_length(), 4);
2066 let qchild = quat
2067 .values()
2068 .as_any()
2069 .downcast_ref::<Float32Array>()
2070 .unwrap();
2071 assert_eq!(
2072 qchild.values(),
2073 &[0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5, 0.5]
2074 );
2075
2076 let rgba = batch
2077 .column_by_name("surfel_rgba")
2078 .unwrap()
2079 .as_any()
2080 .downcast_ref::<FixedSizeListArray>()
2081 .unwrap();
2082 assert_eq!(rgba.value_length(), 4);
2083 let cchild = rgba
2084 .values()
2085 .as_any()
2086 .downcast_ref::<UInt8Array>()
2087 .unwrap();
2088 assert_eq!(cchild.values(), &[255, 0, 0, 128, 0, 255, 0, 255]);
2089 }
2090
2091 #[test]
2092 fn vector_groups_fuse_scalar_columns_at_encode() {
2093 use arrow::array::Float32Array;
2096 set_vector_groups(vec![VectorGroup {
2097 name: "surfel_quat".to_string(),
2098 components: vec!["qx".into(), "qy".into(), "qz".into(), "qw".into()],
2099 elem: VectorElem::F32,
2100 }]);
2101 let layer = ColumnarLayer {
2102 name: "surfels".to_string(),
2103 feature_ids: vec![1, 2],
2104 start_times: vec![0, 10],
2105 end_times: vec![0, 10],
2106 geometry: GeometryColumn::Point(vec![[-122.4, 37.7], [-122.5, 37.8]]),
2107 vertex_times: None,
2108 vertex_values: None,
2109 triangles: None,
2110 vertex_value_matrix: None,
2111 properties: vec![
2112 ("qx".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
2113 ("qy".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
2114 ("qz".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
2115 ("qw".into(), PropertyColumn::Numeric(vec![Some(1.0), Some(0.5)])),
2116 ("z".into(), PropertyColumn::Numeric(vec![Some(3.0), Some(4.0)])),
2117 ],
2118 };
2119 let ipc = encode_layer(&layer).unwrap();
2120 set_vector_groups(Vec::new()); let batch = decode_layer(&ipc).unwrap();
2122
2123 assert!(batch.column_by_name("qx").is_none());
2125 assert!(batch.column_by_name("z").is_some());
2126 let quat = batch
2127 .column_by_name("surfel_quat")
2128 .unwrap()
2129 .as_any()
2130 .downcast_ref::<FixedSizeListArray>()
2131 .unwrap();
2132 assert_eq!(quat.value_length(), 4);
2133 let qchild = quat
2134 .values()
2135 .as_any()
2136 .downcast_ref::<Float32Array>()
2137 .unwrap();
2138 assert_eq!(qchild.values(), &[0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5, 0.5]);
2139 }
2140
2141 #[test]
2142 fn point_elevation_folds_into_3d_geometry_unquantized() {
2143 use arrow::array::Float64Array;
2144 let layer = ColumnarLayer {
2145 name: "cloud".into(),
2146 feature_ids: vec![1, 2],
2147 start_times: vec![0, 0],
2148 end_times: vec![0, 0],
2149 geometry: GeometryColumn::Point(vec![[-122.4, 37.7], [-122.5, 37.8]]),
2150 vertex_times: None,
2151 vertex_values: None,
2152 triangles: None,
2153 vertex_value_matrix: None,
2154 properties: vec![
2155 ("z".into(), PropertyColumn::Numeric(vec![Some(3.5), Some(9.0)])),
2156 ("speed".into(), PropertyColumn::Numeric(vec![Some(1.0), Some(2.0)])),
2157 ],
2158 };
2159 set_point_elevation_column("z");
2160 let ipc = encode_layer(&layer).unwrap();
2161 set_point_elevation_column("");
2162 let batch = decode_layer(&ipc).unwrap();
2163
2164 let geom = batch
2166 .column_by_name("geometry")
2167 .unwrap()
2168 .as_any()
2169 .downcast_ref::<FixedSizeListArray>()
2170 .unwrap();
2171 assert_eq!(geom.value_length(), 3);
2172 let coords = geom.values().as_any().downcast_ref::<Float64Array>().unwrap();
2173 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");
2176 assert!(batch.column_by_name("speed").is_some(), "other props untouched");
2177 }
2178
2179 #[test]
2180 fn point_elevation_3d_geometry_quantizes_with_z_affine() {
2181 use arrow::array::Int32Array;
2182 let layer = ColumnarLayer {
2183 name: "cloud".into(),
2184 feature_ids: vec![1],
2185 start_times: vec![0],
2186 end_times: vec![0],
2187 geometry: GeometryColumn::Point(vec![[-122.4, 37.7]]),
2188 vertex_times: None,
2189 vertex_values: None,
2190 triangles: None,
2191 vertex_value_matrix: None,
2192 properties: vec![("z".into(), PropertyColumn::Numeric(vec![Some(5.0)]))],
2193 };
2194 set_point_elevation_column("z");
2195 let ipc = encode_layer_quantized(&layer, Some(0.05)).unwrap();
2196 set_point_elevation_column("");
2197 let batch = decode_layer(&ipc).unwrap();
2198
2199 let field = batch.schema().field_with_name("geometry").unwrap().clone();
2200 let affine = QuantAffine::from_json(field.metadata().get(STT_QUANT_META_KEY).unwrap()).unwrap();
2201 assert_eq!(affine.z0, Some(0.0));
2202 assert_eq!(affine.sz, Some(0.05));
2203 let geom = batch
2204 .column_by_name("geometry")
2205 .unwrap()
2206 .as_any()
2207 .downcast_ref::<FixedSizeListArray>()
2208 .unwrap();
2209 assert_eq!(geom.value_length(), 3);
2210 let coords = geom.values().as_any().downcast_ref::<Int32Array>().unwrap();
2211 assert_eq!(coords.value(2), 100);
2213 assert_eq!(affine.z0.unwrap() + coords.value(2) as f64 * affine.sz.unwrap(), 5.0);
2214 }
2215
2216 #[test]
2217 fn quantized_point_layer_roundtrips_within_precision() {
2218 let layer = sample_point_layer();
2219 let ipc = encode_layer_quantized(&layer, Some(1.0)).unwrap();
2220 let batch = decode_layer(&ipc).unwrap();
2221
2222 let geom_field = batch.schema().field_with_name("geometry").unwrap().clone();
2224 let affine = QuantAffine::from_json(
2225 geom_field
2226 .metadata()
2227 .get(STT_QUANT_META_KEY)
2228 .expect("quantized tile must carry the affine"),
2229 )
2230 .unwrap();
2231
2232 let geom = batch
2233 .column_by_name("geometry")
2234 .unwrap()
2235 .as_any()
2236 .downcast_ref::<FixedSizeListArray>()
2237 .unwrap();
2238 assert_eq!(geom.value_type(), DataType::Int32);
2239 let coords = geom
2240 .values()
2241 .as_any()
2242 .downcast_ref::<Int32Array>()
2243 .unwrap();
2244
2245 let original = [[-122.4, 37.7], [-122.5, 37.8], [-122.6, 37.9]];
2246 for (i, [lon, lat]) in original.iter().enumerate() {
2247 let rlon = affine.lon(coords.value(i * 2));
2248 let rlat = affine.lat(coords.value(i * 2 + 1));
2249 let dlon_m = (rlon - lon).abs() * M_PER_DEG_LAT * lat.to_radians().cos();
2251 let dlat_m = (rlat - lat).abs() * M_PER_DEG_LAT;
2252 assert!(dlon_m < 1.0, "lon err {dlon_m} m at point {i}");
2253 assert!(dlat_m < 1.0, "lat err {dlat_m} m at point {i}");
2254 }
2255 }
2256
2257 #[test]
2258 fn quantized_numeric_attr_roundtrips_within_precision_and_is_opt_in() {
2259 let zvals: Vec<Option<f64>> =
2263 vec![Some(1.07), Some(-2.4), Some(15.9), None, Some(40.02)];
2264 let make = || ColumnarLayer {
2265 name: "lidar".into(),
2266 feature_ids: vec![1, 2, 3, 4, 5],
2267 start_times: vec![0; 5],
2268 end_times: vec![1; 5],
2269 geometry: GeometryColumn::Point(vec![[-122.4, 37.7]; 5]),
2270 vertex_times: None,
2271 vertex_values: None,
2272 triangles: None,
2273 vertex_value_matrix: None,
2274 properties: vec![("z".into(), PropertyColumn::Numeric(zvals.clone()))],
2275 };
2276
2277 set_quantize_attrs(HashMap::new());
2280 let plain = decode_layer(&encode_layer(&make()).unwrap()).unwrap();
2281 let zf = plain.schema().field_with_name("z").unwrap().clone();
2282 assert_eq!(zf.data_type(), &DataType::Float64);
2283 assert!(zf.metadata().get(STT_QUANT_ATTR_META_KEY).is_none());
2284
2285 set_quantize_attrs(HashMap::from([("z".to_string(), 0.05f64)]));
2287 let q = encode_layer(&make()).unwrap();
2288 set_quantize_attrs(HashMap::new()); let batch = decode_layer(&q).unwrap();
2291 let field = batch.schema().field_with_name("z").unwrap().clone();
2292 assert_eq!(field.data_type(), &DataType::UInt16);
2294 let affine = AttrQuant::from_json(
2295 field
2296 .metadata()
2297 .get(STT_QUANT_ATTR_META_KEY)
2298 .expect("quantized attr must carry the affine"),
2299 )
2300 .unwrap();
2301
2302 let col = batch
2303 .column_by_name("z")
2304 .unwrap()
2305 .as_any()
2306 .downcast_ref::<UInt16Array>()
2307 .unwrap();
2308 for (i, want) in zvals.iter().enumerate() {
2309 match want {
2310 Some(v) => {
2311 assert!(!col.is_null(i), "row {i} should be present");
2312 let got = affine.value(col.value(i) as i64);
2313 assert!((got - v).abs() <= 0.05 / 2.0 + 1e-9, "z[{i}] {got} vs {v}");
2314 }
2315 None => assert!(col.is_null(i), "row {i} should be null"),
2316 }
2317 }
2318 }
2319
2320 #[test]
2321 fn auto_numeric_quantization_is_range_adaptive_and_opt_in() {
2322 let depth: Vec<Option<f64>> = vec![Some(0.0), Some(10.0), Some(123.4), Some(700.0)];
2326 let make = || ColumnarLayer {
2327 name: "q".into(),
2328 feature_ids: vec![1, 2, 3, 4],
2329 start_times: vec![0; 4],
2330 end_times: vec![1; 4],
2331 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; 4]),
2332 vertex_times: None,
2333 vertex_values: None,
2334 triangles: None,
2335 vertex_value_matrix: None,
2336 properties: vec![("depth".into(), PropertyColumn::Numeric(depth.clone()))],
2337 };
2338
2339 set_quantize_attrs_auto(false);
2341 let plain = decode_layer(&encode_layer(&make()).unwrap()).unwrap();
2342 assert_eq!(
2343 plain.schema().field_with_name("depth").unwrap().data_type(),
2344 &DataType::Float64
2345 );
2346
2347 set_quantize_attrs_auto(true);
2349 let batch = decode_layer(&encode_layer(&make()).unwrap()).unwrap();
2350 set_quantize_attrs_auto(false); let field = batch.schema().field_with_name("depth").unwrap().clone();
2353 assert_eq!(field.data_type(), &DataType::UInt16);
2354 let aff = AttrQuant::from_json(field.metadata().get(STT_QUANT_ATTR_META_KEY).unwrap()).unwrap();
2355 let col = batch.column_by_name("depth").unwrap().as_any().downcast_ref::<UInt16Array>().unwrap();
2356 let tol = (700.0 - 0.0) / u16::MAX as f64 / 2.0 + 1e-9;
2357 for (i, want) in depth.iter().enumerate() {
2358 let got = aff.value(col.value(i) as i64);
2359 assert!((got - want.unwrap()).abs() <= tol, "depth[{i}] {got} vs {want:?}");
2360 }
2361 assert_eq!(col.value(0), 0);
2363 assert_eq!(col.value(3), u16::MAX);
2364 }
2365
2366 #[test]
2367 fn quantization_shrinks_geometry_and_is_opt_in() {
2368 let line: Vec<[f64; 2]> = (0..400)
2371 .map(|k| [-73.95 + k as f64 * 1e-4, 40.75 + k as f64 * 7e-5])
2372 .collect();
2373 let layer = ColumnarLayer {
2374 name: "q".into(),
2375 feature_ids: vec![1],
2376 start_times: vec![0],
2377 end_times: vec![1],
2378 geometry: GeometryColumn::LineString(vec![line]),
2379 vertex_times: None,
2380 vertex_values: None,
2381 triangles: None,
2382 vertex_value_matrix: None,
2383 properties: vec![],
2384 };
2385 let plain = encode_layer_quantized(&layer, None).unwrap();
2386 let quant = encode_layer_quantized(&layer, Some(1.0)).unwrap();
2387
2388 let pb = decode_layer(&plain).unwrap();
2390 let pf = pb.schema().field_with_name("geometry").unwrap().clone();
2391 assert!(pf.metadata().get(STT_QUANT_META_KEY).is_none());
2392
2393 let qb = decode_layer(&quant).unwrap();
2395 let qf = qb.schema().field_with_name("geometry").unwrap().clone();
2396 assert!(qf.metadata().get(STT_QUANT_META_KEY).is_some());
2397
2398 assert!(
2400 quant.len() < plain.len(),
2401 "quantized {} should be smaller than f64 {}",
2402 quant.len(),
2403 plain.len()
2404 );
2405 }
2406
2407 #[test]
2408 fn line_layer_roundtrips_with_vertex_times() {
2409 let layer = sample_line_layer();
2410 let ipc = encode_layer(&layer).unwrap();
2411 let batch = decode_layer(&ipc).unwrap();
2412
2413 assert_eq!(batch.num_rows(), 2);
2414 let geom = batch
2415 .column_by_name("geometry")
2416 .unwrap()
2417 .as_any()
2418 .downcast_ref::<ListArray>()
2419 .unwrap();
2420 assert_eq!(geom.value(0).len(), 3);
2422 assert_eq!(geom.value(1).len(), 2);
2423
2424 let meta = batch.schema().metadata().clone();
2427 let origin: i64 = meta
2428 .get("stt:vertex_time_origin_ms")
2429 .expect("u16 vertex-time layers carry an origin")
2430 .parse()
2431 .unwrap();
2432 let step: u32 = meta
2433 .get("stt:vertex_time_step_ms")
2434 .expect("u16 vertex-time layers carry a step")
2435 .parse()
2436 .unwrap();
2437 assert_eq!(origin, 0);
2438 assert_eq!(step, 1);
2439
2440 let vt = batch
2441 .column_by_name("vertex_time")
2442 .unwrap()
2443 .as_any()
2444 .downcast_ref::<ListArray>()
2445 .unwrap();
2446 assert_eq!(vt.len(), 2);
2447 let first = vt.value(0);
2448 let deltas = first.as_any().downcast_ref::<arrow::array::UInt16Array>().unwrap();
2449 let absolutes: Vec<i64> = deltas
2450 .values()
2451 .iter()
2452 .map(|d| origin + (*d as i64) * step as i64)
2453 .collect();
2454 assert_eq!(absolutes, vec![0, 25, 50]);
2455 }
2456
2457 #[test]
2458 fn line_layer_roundtrips_with_vertex_values() {
2459 let layer = ColumnarLayer {
2462 name: "drift".into(),
2463 feature_ids: vec![1, 2],
2464 start_times: vec![0, 0],
2465 end_times: vec![100, 100],
2466 geometry: GeometryColumn::LineString(vec![
2467 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
2468 vec![[3.0, 3.0], [4.0, 4.0]],
2469 ]),
2470 vertex_times: None,
2471 vertex_values: Some(vec![vec![5.0, f32::NAN, 27.5], vec![12.0, 13.0]]),
2472 triangles: None,
2473 vertex_value_matrix: None,
2474 properties: vec![],
2475 };
2476 let ipc = encode_layer(&layer).unwrap();
2477 let batch = decode_layer(&ipc).unwrap();
2478
2479 let vv = batch
2480 .column_by_name("vertex_value")
2481 .expect("layers with per-vertex values carry a vertex_value column")
2482 .as_any()
2483 .downcast_ref::<ListArray>()
2484 .unwrap();
2485 assert_eq!(vv.len(), 2);
2486 let first = vv.value(0);
2487 let vals = first.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
2488 assert_eq!(vals.value(0), 5.0);
2489 assert!(vals.value(1).is_nan());
2490 assert_eq!(vals.value(2), 27.5);
2491 let second = vv.value(1);
2492 let vals2 = second.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
2493 assert_eq!(vals2.values(), &[12.0, 13.0]);
2494 }
2495
2496 #[test]
2497 fn line_layer_roundtrips_with_vertex_value_matrix() {
2498 let layer = ColumnarLayer {
2503 name: "flows".into(),
2504 feature_ids: vec![1, 2],
2505 start_times: vec![0, 0],
2506 end_times: vec![1800, 1800],
2507 geometry: GeometryColumn::LineString(vec![
2508 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
2509 vec![[3.0, 3.0], [4.0, 4.0]],
2510 ]),
2511 vertex_times: None,
2512 vertex_values: None,
2513 triangles: None,
2514 vertex_value_matrix: Some(vec![
2516 vec![10.0, 11.0, 20.0, 21.0, 30.0, 31.0],
2517 vec![40.0, 41.0, 50.0, 51.0],
2518 ]),
2519 properties: vec![],
2520 };
2521 let ipc = encode_layer(&layer).unwrap();
2522 let batch = decode_layer(&ipc).unwrap();
2523
2524 let vm = batch
2525 .column_by_name("vertex_value_matrix")
2526 .expect("matrix layers carry a vertex_value_matrix column")
2527 .as_any()
2528 .downcast_ref::<ListArray>()
2529 .unwrap();
2530 assert_eq!(vm.len(), 2);
2531 let f0 = vm.value(0);
2532 let f0v = f0.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
2533 assert_eq!(f0v.values(), &[10.0, 11.0, 20.0, 21.0, 30.0, 31.0]);
2534 let f1 = vm.value(1);
2535 let f1v = f1.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
2536 assert_eq!(f1v.values(), &[40.0, 41.0, 50.0, 51.0]);
2537
2538 assert_eq!(
2540 batch.schema().metadata().get("stt:vertex_value_buckets"),
2541 Some(&"2".to_string())
2542 );
2543 }
2544
2545 #[test]
2546 fn vertex_time_falls_back_to_int64_for_wide_spans() {
2547 let layer = ColumnarLayer {
2552 name: "edge".into(),
2553 feature_ids: vec![1],
2554 start_times: vec![0],
2555 end_times: vec![100],
2556 geometry: GeometryColumn::LineString(vec![vec![[0.0, 0.0], [1.0, 1.0]]]),
2557 vertex_times: Some(vec![vec![0, 100_000_000_000]]),
2558 vertex_values: None,
2559 triangles: None,
2560 vertex_value_matrix: None,
2561 properties: vec![],
2562 };
2563 let ipc = encode_layer(&layer).unwrap();
2564 let batch = decode_layer(&ipc).unwrap();
2565 let schema = batch.schema();
2566 let meta = schema.metadata();
2567 assert!(meta.get("stt:vertex_time_origin_ms").is_none());
2568 assert!(meta.get("stt:vertex_time_step_ms").is_none());
2569 let vt = batch
2570 .column_by_name("vertex_time")
2571 .unwrap()
2572 .as_any()
2573 .downcast_ref::<ListArray>()
2574 .unwrap();
2575 let first = vt.value(0);
2576 let absolutes = first
2577 .as_any()
2578 .downcast_ref::<Int64Array>()
2579 .expect("wide spans must keep the exact Int64 shape");
2580 assert_eq!(absolutes.values(), &[0, 100_000_000_000]);
2581 }
2582
2583 #[test]
2584 fn vertex_time_step_ceiling_is_the_u16_vs_int64_threshold() {
2585 let make = |span: i64| ColumnarLayer {
2588 name: "edge".into(),
2589 feature_ids: vec![1],
2590 start_times: vec![0],
2591 end_times: vec![100],
2592 geometry: GeometryColumn::LineString(vec![vec![[0.0, 0.0], [1.0, 1.0]]]),
2593 vertex_times: Some(vec![vec![0, span]]),
2594 vertex_values: None,
2595 triangles: None,
2596 vertex_value_matrix: None,
2597 properties: vec![],
2598 };
2599
2600 let at_ceiling = decode_layer(&encode_layer(&make(65_535_000)).unwrap()).unwrap();
2601 let schema = at_ceiling.schema();
2602 let step: u32 = schema
2603 .metadata()
2604 .get("stt:vertex_time_step_ms")
2605 .expect("span at the ceiling stays u16-delta encoded")
2606 .parse()
2607 .unwrap();
2608 assert_eq!(step, DEFAULT_VERTEX_TIME_MAX_STEP_MS);
2609
2610 let past_ceiling = decode_layer(&encode_layer(&make(65_536_000)).unwrap()).unwrap();
2611 assert!(past_ceiling
2612 .schema()
2613 .metadata()
2614 .get("stt:vertex_time_step_ms")
2615 .is_none());
2616 let vt = past_ceiling
2617 .column_by_name("vertex_time")
2618 .unwrap()
2619 .as_any()
2620 .downcast_ref::<ListArray>()
2621 .unwrap();
2622 let first = vt.value(0);
2623 let absolutes = first.as_any().downcast_ref::<Int64Array>().unwrap();
2624 assert_eq!(absolutes.values(), &[0, 65_536_000]);
2625 }
2626
2627 #[test]
2628 fn polygon_layer_roundtrips_with_rings() {
2629 let layer = sample_polygon_layer();
2630 let ipc = encode_layer(&layer).unwrap();
2631 let batch = decode_layer(&ipc).unwrap();
2632
2633 let geom = batch
2634 .column_by_name("geometry")
2635 .unwrap()
2636 .as_any()
2637 .downcast_ref::<ListArray>()
2638 .unwrap();
2639 assert_eq!(geom.len(), 1);
2640 let rings = geom.value(0);
2642 let rings = rings.as_any().downcast_ref::<ListArray>().unwrap();
2643 assert_eq!(rings.len(), 2);
2644 assert_eq!(rings.value(0).len(), 5); assert_eq!(rings.value(1).len(), 5); }
2647
2648 #[test]
2649 fn multi_layer_tile_frame_roundtrips() {
2650 let layers = vec![sample_line_layer(), sample_point_layer()];
2651 let payload = encode_tile(&layers).unwrap();
2652 let decoded = decode_tile(&payload).unwrap();
2653
2654 assert_eq!(decoded.len(), 2);
2655 assert_eq!(decoded[0].name, "tracks");
2656 assert_eq!(decoded[1].name, "points");
2657 assert_eq!(decoded[0].batch.num_rows(), 2);
2658 assert_eq!(decoded[1].batch.num_rows(), 3);
2659 assert_eq!(
2661 decoded[1]
2662 .batch
2663 .schema()
2664 .metadata()
2665 .get("stt:layer")
2666 .map(String::as_str),
2667 Some("points")
2668 );
2669 }
2670
2671 #[test]
2672 fn tessellate_polygon_emits_two_triangles_for_a_square() {
2673 let ring: Vec<Coord> = vec![
2676 [0.0, 0.0],
2677 [1.0, 0.0],
2678 [1.0, 1.0],
2679 [0.0, 1.0],
2680 [0.0, 0.0],
2681 ];
2682 let tris = tessellate_polygon(&[ring]);
2683 assert_eq!(tris.len(), 6);
2684 for &i in &tris {
2685 assert!(i < 5);
2686 }
2687 }
2688
2689 #[test]
2690 fn tessellate_polygon_handles_a_hole() {
2691 let exterior: Vec<Coord> =
2695 vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]];
2696 let hole: Vec<Coord> =
2697 vec![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [1.0, 1.0]];
2698 let tris = tessellate_polygon(&[exterior, hole]);
2699 assert!(tris.len() >= 6);
2700 assert_eq!(tris.len() % 3, 0);
2701 for &i in &tris {
2702 assert!(i < 10);
2703 }
2704 }
2705
2706 #[test]
2707 fn tessellate_polygon_handles_degenerate_input() {
2708 assert!(tessellate_polygon(&[]).is_empty());
2710 let degenerate: Vec<Coord> = vec![[0.0, 0.0], [1.0, 1.0]];
2712 assert!(tessellate_polygon(&[degenerate]).is_empty());
2713 }
2714
2715 #[test]
2716 fn polygon_layer_with_triangles_roundtrips() {
2717 let exterior: Vec<Coord> =
2718 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
2719 let tris = tessellate_polygon(&[exterior.clone()]);
2720 assert_eq!(tris.len(), 6);
2721 let layer = ColumnarLayer {
2722 name: "zones".into(),
2723 feature_ids: vec![42],
2724 start_times: vec![0],
2725 end_times: vec![1000],
2726 geometry: GeometryColumn::Polygon(vec![vec![exterior]]),
2727 vertex_times: None,
2728 vertex_values: None,
2729 triangles: Some(vec![tris.clone()]),
2730 vertex_value_matrix: None,
2731 properties: vec![],
2732 };
2733 let ipc = encode_layer(&layer).unwrap();
2734 let batch = decode_layer(&ipc).unwrap();
2735
2736 assert_eq!(
2738 batch
2739 .schema()
2740 .metadata()
2741 .get(TRIANGLES_METADATA_KEY)
2742 .map(String::as_str),
2743 Some("true")
2744 );
2745 let col = batch
2748 .column_by_name("triangles")
2749 .expect("triangles column present")
2750 .as_any()
2751 .downcast_ref::<ListArray>()
2752 .expect("triangles is a List");
2753 assert_eq!(col.len(), 1);
2754 let first = col.value(0);
2755 let values: &arrow::array::UInt16Array = first
2756 .as_any()
2757 .downcast_ref::<arrow::array::UInt16Array>()
2758 .expect("triangle values are UInt16 for small feature-local indices");
2759 assert_eq!(
2760 values.values().iter().map(|&v| v as u32).collect::<Vec<_>>(),
2761 tris
2762 );
2763 }
2764
2765 #[test]
2766 fn polygon_layer_with_oversized_triangle_index_falls_back_to_uint32() {
2767 let exterior: Vec<Coord> =
2771 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
2772 let big_tris = vec![0u32, 1, 70_000];
2773 let layer = ColumnarLayer {
2774 name: "zones".into(),
2775 feature_ids: vec![42],
2776 start_times: vec![0],
2777 end_times: vec![1000],
2778 geometry: GeometryColumn::Polygon(vec![vec![exterior]]),
2779 vertex_times: None,
2780 vertex_values: None,
2781 triangles: Some(vec![big_tris.clone()]),
2782 vertex_value_matrix: None,
2783 properties: vec![],
2784 };
2785 let ipc = encode_layer(&layer).unwrap();
2786 let batch = decode_layer(&ipc).unwrap();
2787
2788 let col = batch
2789 .column_by_name("triangles")
2790 .expect("triangles column present")
2791 .as_any()
2792 .downcast_ref::<ListArray>()
2793 .expect("triangles is a List");
2794 let first = col.value(0);
2795 let values: &arrow::array::UInt32Array = first
2796 .as_any()
2797 .downcast_ref::<arrow::array::UInt32Array>()
2798 .expect("triangle values fall back to UInt32 when an index exceeds u16::MAX");
2799 assert_eq!(values.values().to_vec(), big_tris);
2800 }
2801
2802 #[test]
2803 fn polygon_layer_without_triangles_skips_the_metadata_key() {
2804 let layer = sample_polygon_layer();
2808 let ipc = encode_layer(&layer).unwrap();
2809 let batch = decode_layer(&ipc).unwrap();
2810 assert!(!batch.schema().metadata().contains_key(TRIANGLES_METADATA_KEY));
2811 assert!(batch.column_by_name("triangles").is_none());
2812 }
2813
2814 #[test]
2815 fn non_polygon_layer_drops_stray_triangles() {
2816 let mut layer = sample_point_layer();
2820 layer.triangles = Some(vec![vec![0, 1, 2]; layer.feature_ids.len()]);
2822 let ipc = encode_layer(&layer).unwrap();
2823 let batch = decode_layer(&ipc).unwrap();
2824 assert!(!batch.schema().metadata().contains_key(TRIANGLES_METADATA_KEY));
2825 assert!(batch.column_by_name("triangles").is_none());
2826 }
2827
2828 fn ipc_offsets(payload: &[u8]) -> Vec<(usize, usize)> {
2831 let raw = u16::from_le_bytes([payload[0], payload[1]]);
2832 let aligned = raw & ALIGNED_FRAME_FLAG != 0;
2833 let count = (raw & !ALIGNED_FRAME_FLAG) as usize;
2834 let mut pos = 2usize;
2835 let mut out = Vec::new();
2836 for _ in 0..count {
2837 let name_len =
2838 u16::from_le_bytes([payload[pos], payload[pos + 1]]) as usize;
2839 pos += 2 + name_len;
2840 let ipc_len = u32::from_le_bytes([
2841 payload[pos],
2842 payload[pos + 1],
2843 payload[pos + 2],
2844 payload[pos + 3],
2845 ]) as usize;
2846 pos += 4;
2847 if aligned {
2848 pos += (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
2849 }
2850 out.push((pos, ipc_len));
2851 pos += ipc_len;
2852 }
2853 out
2854 }
2855
2856 #[test]
2857 fn encoded_frames_align_every_ipc_stream_to_8_bytes() {
2858 let mut a = sample_line_layer();
2862 a.name = "x".into();
2863 let mut b = sample_point_layer();
2864 b.name = "a-longer-layer-name".into();
2865 let payload = encode_tile(&[a, b]).unwrap();
2866
2867 let raw = u16::from_le_bytes([payload[0], payload[1]]);
2868 assert_ne!(raw & ALIGNED_FRAME_FLAG, 0, "writer must set the aligned flag");
2869
2870 let offsets = ipc_offsets(&payload);
2871 assert_eq!(offsets.len(), 2);
2872 for (off, _) in &offsets {
2873 assert_eq!(off % 8, 0, "IPC stream at offset {off} is misaligned");
2874 }
2875
2876 let decoded = decode_tile(&payload).unwrap();
2878 assert_eq!(decoded[0].name, "x");
2879 assert_eq!(decoded[1].name, "a-longer-layer-name");
2880 assert_eq!(decoded[0].batch.num_rows(), 2);
2881 assert_eq!(decoded[1].batch.num_rows(), 3);
2882 }
2883
2884 #[test]
2885 fn legacy_unpadded_frames_still_decode() {
2886 let layers = vec![sample_line_layer(), sample_point_layer()];
2890 let aligned_payload = encode_tile(&layers).unwrap();
2891 let aligned = decode_tile(&aligned_payload).unwrap();
2892
2893 let mut legacy: Vec<u8> = Vec::new();
2894 legacy.extend_from_slice(&(layers.len() as u16).to_le_bytes());
2895 for ((off, len), layer) in ipc_offsets(&aligned_payload).iter().zip(&layers) {
2896 let name = layer.name.as_bytes();
2897 legacy.extend_from_slice(&(name.len() as u16).to_le_bytes());
2898 legacy.extend_from_slice(name);
2899 legacy.extend_from_slice(&(*len as u32).to_le_bytes());
2900 legacy.extend_from_slice(&aligned_payload[*off..*off + *len]);
2901 }
2902
2903 let decoded = decode_tile(&legacy).unwrap();
2904 assert_eq!(decoded.len(), aligned.len());
2905 for (l, a) in decoded.iter().zip(&aligned) {
2906 assert_eq!(l.name, a.name);
2907 assert_eq!(l.batch, a.batch);
2908 }
2909 }
2910
2911 #[test]
2912 fn truncated_tile_frame_errors_cleanly() {
2913 let payload = encode_tile(&[sample_point_layer()]).unwrap();
2914 let truncated = &payload[..payload.len() / 2];
2916 assert!(decode_tile(truncated).is_err());
2917 }
2918
2919 #[test]
2920 fn length_mismatch_is_rejected() {
2921 let mut layer = sample_point_layer();
2922 layer.start_times.pop(); assert!(encode_layer(&layer).is_err());
2924 }
2925}