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();
1326 geom_meta.insert(
1327 GEOARROW_EXT_KEY.to_string(),
1328 layer.geometry.geoarrow_name().to_string(),
1329 );
1330 match &quant {
1331 Some(q) => {
1339 geom_meta.insert(STT_QUANT_META_KEY.to_string(), q.to_json());
1340 }
1341 None => {
1343 geom_meta.insert(
1344 GEOARROW_EXT_META_KEY.to_string(),
1345 GEOARROW_CRS_METADATA.to_string(),
1346 );
1347 }
1348 }
1349 fields.push(Arc::new(
1350 Field::new("geometry", geom_array.data_type().clone(), false)
1351 .with_metadata(geom_meta.into_iter().collect()),
1352 ));
1353 columns.push(geom_array);
1354
1355 let mut vertex_time_encoding: Option<(i64, u32)> = None;
1358 if let Some(vt_col) = build_vertex_time_array(&layer.vertex_times, n, cfg.vertex_time_max_step_ms) {
1359 fields.push(Arc::new(Field::new(
1360 "vertex_time",
1361 vt_col.array.data_type().clone(),
1362 true,
1363 )));
1364 columns.push(vt_col.array);
1365 vertex_time_encoding = vt_col.encoding;
1366 }
1367
1368 if let Some(vv_array) = build_vertex_value_array(&layer.vertex_values, n) {
1371 fields.push(Arc::new(Field::new(
1372 "vertex_value",
1373 vv_array.data_type().clone(),
1374 true,
1375 )));
1376 columns.push(vv_array);
1377 }
1378
1379 let mut vertex_value_buckets: Option<u32> = None;
1384 if let Some(vm_array) = build_vertex_value_array(&layer.vertex_value_matrix, n) {
1385 fields.push(Arc::new(Field::new(
1386 "vertex_value_matrix",
1387 vm_array.data_type().clone(),
1388 true,
1389 )));
1390 columns.push(vm_array);
1391 vertex_value_buckets = layer
1392 .vertex_value_matrix
1393 .as_ref()
1394 .and_then(|vm| infer_vertex_value_buckets(vm, &layer.geometry));
1395 }
1396
1397 let has_triangles = matches!(layer.geometry, GeometryColumn::Polygon(_))
1401 && layer
1402 .triangles
1403 .as_ref()
1404 .map(|t| t.iter().any(|f| !f.is_empty()))
1405 .unwrap_or(false);
1406 if has_triangles {
1407 let tri = layer.triangles.as_ref().unwrap();
1408 let max_index = tri.iter().flatten().copied().max().unwrap_or(0);
1417 let array: ArrayRef = if max_index <= u16::MAX as u32 {
1418 let mut builder = ListBuilder::new(UInt16Builder::new());
1419 for feature in tri {
1420 for &idx in feature {
1421 builder.values().append_value(idx as u16);
1422 }
1423 builder.append(true);
1426 }
1427 Arc::new(builder.finish())
1428 } else {
1429 let mut builder = ListBuilder::new(UInt32Builder::new());
1430 for feature in tri {
1431 for &idx in feature {
1432 builder.values().append_value(idx);
1433 }
1434 builder.append(true);
1435 }
1436 Arc::new(builder.finish())
1437 };
1438 fields.push(Arc::new(Field::new(
1439 "triangles",
1440 array.data_type().clone(),
1441 false,
1442 )));
1443 columns.push(array);
1444 }
1445
1446 let grouped =
1451 group_vector_properties(&layer.properties, layer.feature_count(), &cfg.vector_groups);
1452 let props_iter: &[(String, PropertyColumn)] =
1453 grouped.as_deref().unwrap_or(&layer.properties);
1454 for (name, col) in props_iter {
1455 if elev_consumed && name == &elev_col {
1458 continue;
1459 }
1460 match col {
1461 PropertyColumn::Numeric(values) => {
1462 let quantized = cfg
1469 .quantize_attrs
1470 .get(name)
1471 .copied()
1472 .filter(|p| *p > 0.0)
1473 .and_then(|p| build_quantized_numeric(values, p))
1474 .or_else(|| {
1475 cfg.quantize_attrs_auto
1478 .then(|| build_quantized_numeric_auto(values))
1479 .flatten()
1480 });
1481 match quantized {
1482 Some((array, affine_json)) => {
1483 let mut m = HashMap::new();
1484 m.insert(STT_QUANT_ATTR_META_KEY.to_string(), affine_json);
1485 fields.push(Arc::new(
1486 Field::new(name, array.data_type().clone(), true).with_metadata(m),
1487 ));
1488 columns.push(array);
1489 }
1490 None => {
1491 fields.push(Arc::new(Field::new(name, DataType::Float64, true)));
1492 columns.push(Arc::new(Float64Array::from(values.clone())));
1493 }
1494 }
1495 }
1496 PropertyColumn::Categorical(values) => {
1497 let (indices, categories) = build_dictionary_indices(values)?;
1501 let key_type = DataType::UInt16;
1502 let value_type = DataType::Utf8;
1503 let dict_type = DataType::Dictionary(Box::new(key_type), Box::new(value_type));
1504 fields.push(Arc::new(Field::new(name, dict_type, true)));
1505
1506 let value_array: ArrayRef = Arc::new(StringArray::from(
1507 categories.iter().map(|s| Some(s.as_str())).collect::<Vec<_>>(),
1508 ));
1509 let key_array = UInt16Array::from(indices);
1510 let dict = DictionaryArray::<UInt16Type>::try_new(key_array, value_array)
1511 .map_err(|e| Error::Other(format!("dictionary build failed: {e}")))?;
1512 columns.push(Arc::new(dict));
1513 }
1514 PropertyColumn::Vector { width, elem, values } => {
1515 let (child, child_dt): (ArrayRef, DataType) = match elem {
1521 VectorElem::F32 => (
1522 Arc::new(Float32Array::from(values.clone())),
1523 DataType::Float32,
1524 ),
1525 VectorElem::U8 => {
1526 let bytes: Vec<u8> = values
1527 .iter()
1528 .map(|v| v.round().clamp(0.0, 255.0) as u8)
1529 .collect();
1530 (Arc::new(UInt8Array::from(bytes)), DataType::UInt8)
1531 }
1532 };
1533 let item_field = Arc::new(Field::new("item", child_dt, false));
1534 let fsl = FixedSizeListArray::new(item_field, *width as i32, child, None);
1535 fields.push(Arc::new(Field::new(
1536 name,
1537 fsl.data_type().clone(),
1538 true,
1539 )));
1540 columns.push(Arc::new(fsl));
1541 }
1542 }
1543 }
1544
1545 let mut schema_meta: BTreeMap<String, String> = BTreeMap::new();
1558 schema_meta.insert("stt:layer".to_string(), layer.name.clone());
1559 schema_meta.insert(
1560 "stt:geometry".to_string(),
1561 layer.geometry.geoarrow_name().to_string(),
1562 );
1563 if let Some(min_start) = layer.start_times.iter().copied().min() {
1569 schema_meta.insert(TIME_OFFSET_MS_KEY.to_string(), min_start.to_string());
1570 }
1571 if let Some((origin, step)) = vertex_time_encoding {
1572 schema_meta.insert(VERTEX_TIME_ORIGIN_KEY.to_string(), origin.to_string());
1573 schema_meta.insert(VERTEX_TIME_STEP_KEY.to_string(), step.to_string());
1574 }
1575 if let Some(buckets) = vertex_value_buckets {
1576 schema_meta.insert(VERTEX_VALUE_BUCKETS_KEY.to_string(), buckets.to_string());
1577 }
1578 if has_triangles {
1579 schema_meta.insert(TRIANGLES_METADATA_KEY.to_string(), "true".to_string());
1580 }
1581 let schema = Arc::new(Schema::new(fields).with_metadata(schema_meta.into_iter().collect()));
1582
1583 let batch = RecordBatch::try_new(schema.clone(), columns)
1584 .map_err(|e| Error::Other(format!("failed to build tile RecordBatch: {e}")))?;
1585
1586 let mut buf = Vec::new();
1587 {
1588 let mut writer = StreamWriter::try_new(&mut buf, &schema)
1589 .map_err(|e| Error::Other(format!("Arrow IPC writer init failed: {e}")))?;
1590 writer
1591 .write(&batch)
1592 .map_err(|e| Error::Other(format!("Arrow IPC write failed: {e}")))?;
1593 writer
1594 .finish()
1595 .map_err(|e| Error::Other(format!("Arrow IPC finish failed: {e}")))?;
1596 }
1597 Ok(buf)
1598}
1599
1600pub fn encode_tile(layers: &[ColumnarLayer]) -> Result<Vec<u8>> {
1608 encode_tile_cfg(layers, &EncoderConfig::from_globals())
1609}
1610
1611pub fn encode_tile_quantized(layers: &[ColumnarLayer], quantize_m: Option<f64>) -> Result<Vec<u8>> {
1616 encode_tile_cfg(
1617 layers,
1618 &EncoderConfig {
1619 quantize_coords_m: quantize_m,
1620 ..EncoderConfig::from_globals()
1621 },
1622 )
1623}
1624
1625pub fn encode_tile_with(layers: &[ColumnarLayer], cfg: &EncoderConfig) -> Result<Vec<u8>> {
1630 encode_tile_cfg(layers, cfg)
1631}
1632
1633fn encode_tile_cfg(layers: &[ColumnarLayer], cfg: &EncoderConfig) -> Result<Vec<u8>> {
1636 if layers.len() >= ALIGNED_FRAME_FLAG as usize {
1637 return Err(Error::Other(format!(
1638 "tile has {} layers, exceeds the {} frame limit",
1639 layers.len(),
1640 ALIGNED_FRAME_FLAG - 1
1641 )));
1642 }
1643 let mut out = Vec::new();
1644 out.extend_from_slice(&(layers.len() as u16 | ALIGNED_FRAME_FLAG).to_le_bytes());
1645 for layer in layers {
1646 let name = layer.name.as_bytes();
1647 if name.len() > u16::MAX as usize {
1648 return Err(Error::Other("layer name too long".into()));
1649 }
1650 let ipc = encode_layer_cfg(layer, cfg)?;
1651 out.extend_from_slice(&(name.len() as u16).to_le_bytes());
1652 out.extend_from_slice(name);
1653 out.extend_from_slice(&(ipc.len() as u32).to_le_bytes());
1654 let pad = (FRAME_ALIGN - out.len() % FRAME_ALIGN) % FRAME_ALIGN;
1655 out.extend_from_slice(&[0u8; FRAME_ALIGN][..pad]);
1656 out.extend_from_slice(&ipc);
1657 }
1658 Ok(out)
1659}
1660
1661#[derive(Debug, Clone)]
1667pub struct DecodedLayer {
1668 pub name: String,
1670 pub batch: RecordBatch,
1672}
1673
1674pub fn decode_layer(ipc: &[u8]) -> Result<RecordBatch> {
1676 let reader = StreamReader::try_new(ipc, None)
1677 .map_err(|e| Error::Other(format!("Arrow IPC reader init failed: {e}")))?;
1678 let mut batches: Vec<RecordBatch> = Vec::new();
1679 for batch in reader {
1680 batches.push(batch.map_err(|e| Error::Other(format!("Arrow IPC read failed: {e}")))?);
1681 }
1682 match batches.len() {
1683 0 => Err(Error::Other("tile layer IPC contained no record batch".into())),
1684 1 => Ok(batches.into_iter().next().unwrap()),
1685 _ => arrow::compute::concat_batches(&batches[0].schema(), &batches)
1688 .map_err(|e| Error::Other(format!("failed to concat tile batches: {e}"))),
1689 }
1690}
1691
1692pub fn decode_tile(payload: &[u8]) -> Result<Vec<DecodedLayer>> {
1698 if payload.len() < 2 {
1699 return Err(Error::Other("tile payload too short for layer frame".into()));
1700 }
1701 let raw_count = u16::from_le_bytes([payload[0], payload[1]]);
1702 let aligned = raw_count & ALIGNED_FRAME_FLAG != 0;
1703 let count = (raw_count & !ALIGNED_FRAME_FLAG) as usize;
1704 let mut pos = 2usize;
1705 let mut layers = Vec::with_capacity(count);
1706 for _ in 0..count {
1707 let name_len = read_u16(payload, &mut pos)? as usize;
1708 let name = read_slice(payload, &mut pos, name_len)?;
1709 let name = String::from_utf8(name.to_vec())
1710 .map_err(|e| Error::Other(format!("layer name not utf8: {e}")))?;
1711 let ipc_len = read_u32(payload, &mut pos)? as usize;
1712 if aligned {
1713 let pad = (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
1714 read_slice(payload, &mut pos, pad)?;
1715 }
1716 let ipc = read_slice(payload, &mut pos, ipc_len)?;
1717 let batch = decode_layer(ipc)?;
1718 layers.push(DecodedLayer { name, batch });
1719 }
1720 Ok(layers)
1721}
1722
1723fn read_u16(buf: &[u8], pos: &mut usize) -> Result<u16> {
1724 let s = read_slice(buf, pos, 2)?;
1725 Ok(u16::from_le_bytes([s[0], s[1]]))
1726}
1727
1728fn read_u32(buf: &[u8], pos: &mut usize) -> Result<u32> {
1729 let s = read_slice(buf, pos, 4)?;
1730 Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
1731}
1732
1733fn read_slice<'a>(buf: &'a [u8], pos: &mut usize, len: usize) -> Result<&'a [u8]> {
1734 let end = pos
1735 .checked_add(len)
1736 .ok_or_else(|| Error::Other("tile frame length overflow".into()))?;
1737 if end > buf.len() {
1738 return Err(Error::Other("tile frame truncated".into()));
1739 }
1740 let s = &buf[*pos..end];
1741 *pos = end;
1742 Ok(s)
1743}
1744
1745#[cfg(test)]
1746mod tests {
1747 use super::*;
1748
1749 fn sample_point_layer() -> ColumnarLayer {
1750 ColumnarLayer {
1751 name: "points".to_string(),
1752 feature_ids: vec![1, 2, 3],
1753 start_times: vec![1000, 2000, 3000],
1754 end_times: vec![1500, 2500, 3500],
1755 geometry: GeometryColumn::Point(vec![
1756 [-122.4, 37.7],
1757 [-122.5, 37.8],
1758 [-122.6, 37.9],
1759 ]),
1760 vertex_times: None,
1761 vertex_values: None,
1762 triangles: None,
1763 vertex_value_matrix: None,
1764 properties: vec![
1765 (
1766 "speed".to_string(),
1767 PropertyColumn::Numeric(vec![Some(10.0), None, Some(30.0)]),
1768 ),
1769 (
1770 "kind".to_string(),
1771 PropertyColumn::Categorical(vec![
1772 Some("car".to_string()),
1773 Some("bus".to_string()),
1774 None,
1775 ]),
1776 ),
1777 ],
1778 }
1779 }
1780
1781 #[test]
1788 fn encode_tile_with_is_config_driven_not_global() {
1789 let layer = sample_point_layer();
1790 let layers = std::slice::from_ref(&layer);
1791
1792 let plain_cfg = EncoderConfig::default();
1793 let quant_cfg = EncoderConfig {
1794 quantize_coords_m: Some(1.0),
1795 ..EncoderConfig::default()
1796 };
1797 let attr_cfg = EncoderConfig {
1798 quantize_attrs_auto: true,
1799 ..EncoderConfig::default()
1800 };
1801
1802 let plain = encode_tile_with(layers, &plain_cfg).unwrap();
1803 let quant = encode_tile_with(layers, &quant_cfg).unwrap();
1804 let attr = encode_tile_with(layers, &attr_cfg).unwrap();
1805
1806 assert_ne!(plain, quant, "coord quantization must change the tile");
1816 assert_ne!(plain, attr, "attribute quantization must change the tile");
1817 assert_ne!(quant, attr, "the two quantizations differ from each other");
1818
1819 for tile in [&plain, &quant, &attr] {
1822 let rows: usize = decode_tile(tile).unwrap().iter().map(|l| l.batch.num_rows()).sum();
1823 assert_eq!(rows, 3);
1824 }
1825 }
1826
1827 fn sample_line_layer() -> ColumnarLayer {
1828 ColumnarLayer {
1829 name: "tracks".to_string(),
1830 feature_ids: vec![10, 11],
1831 start_times: vec![0, 100],
1832 end_times: vec![50, 200],
1833 geometry: GeometryColumn::LineString(vec![
1834 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
1835 vec![[5.0, 5.0], [6.0, 6.0]],
1836 ]),
1837 vertex_times: Some(vec![vec![0, 25, 50], vec![100, 200]]),
1838 vertex_values: None,
1839 triangles: None,
1840 vertex_value_matrix: None,
1841 properties: vec![],
1842 }
1843 }
1844
1845 fn sample_polygon_layer() -> ColumnarLayer {
1846 ColumnarLayer {
1847 name: "zones".to_string(),
1848 feature_ids: vec![42],
1849 start_times: vec![0],
1850 end_times: vec![1000],
1851 geometry: GeometryColumn::Polygon(vec![vec![
1852 vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]],
1854 vec![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [1.0, 1.0]],
1856 ]]),
1857 vertex_times: None,
1858 vertex_values: None,
1859 triangles: None,
1860 vertex_value_matrix: None,
1861 properties: vec![],
1862 }
1863 }
1864
1865 #[test]
1866 fn categorical_columns_use_dictionary_encoding() {
1867 let layer = ColumnarLayer {
1868 name: "cars".into(),
1869 feature_ids: vec![1, 2, 3, 4, 5],
1870 start_times: vec![0; 5],
1871 end_times: vec![1; 5],
1872 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; 5]),
1873 vertex_times: None,
1874 vertex_values: None,
1875 triangles: None,
1876 vertex_value_matrix: None,
1877 properties: vec![(
1878 "kind".into(),
1879 PropertyColumn::Categorical(vec![
1880 Some("car".into()),
1881 Some("bus".into()),
1882 Some("car".into()),
1883 None,
1884 Some("car".into()),
1885 ]),
1886 )],
1887 };
1888 let ipc = encode_layer(&layer).unwrap();
1889 let batch = decode_layer(&ipc).unwrap();
1890 let field = batch.schema().field_with_name("kind").unwrap().clone();
1891 match field.data_type() {
1892 DataType::Dictionary(k, v) => {
1893 assert_eq!(k.as_ref(), &DataType::UInt16);
1894 assert_eq!(v.as_ref(), &DataType::Utf8);
1895 }
1896 other => panic!("expected Dictionary<UInt16, Utf8>, got {other:?}"),
1897 }
1898
1899 let col = batch
1900 .column_by_name("kind")
1901 .unwrap()
1902 .as_any()
1903 .downcast_ref::<DictionaryArray<UInt16Type>>()
1904 .unwrap();
1905 let values = col
1906 .values()
1907 .as_any()
1908 .downcast_ref::<StringArray>()
1909 .unwrap();
1910 let mut categories: Vec<&str> = (0..values.len()).map(|i| values.value(i)).collect();
1912 categories.sort();
1913 assert_eq!(categories, vec!["bus", "car"]);
1914
1915 assert!(col.is_null(3));
1917 let keys = col.keys();
1918 for i in [0usize, 1, 2, 4] {
1919 assert!(keys.value(i) < values.len() as u16);
1920 }
1921 }
1922
1923 #[test]
1924 fn categorical_overflow_errors_instead_of_corrupting() {
1925 let n = u16::MAX as usize + 1; let kinds: Vec<Option<String>> = (0..n).map(|i| Some(format!("c{i}"))).collect();
1929 let layer = ColumnarLayer {
1930 name: "huge".into(),
1931 feature_ids: (0..n as u64).collect(),
1932 start_times: vec![0; n],
1933 end_times: vec![1; n],
1934 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; n]),
1935 vertex_times: None,
1936 vertex_values: None,
1937 triangles: None,
1938 vertex_value_matrix: None,
1939 properties: vec![("kind".into(), PropertyColumn::Categorical(kinds))],
1940 };
1941 let err = encode_layer(&layer).expect_err("overflowing dictionary must error");
1942 assert!(
1943 err.to_string().contains("distinct values"),
1944 "unexpected error: {err}"
1945 );
1946 }
1947
1948 #[test]
1949 fn geometry_field_advertises_crs_metadata() {
1950 for layer in [sample_point_layer(), sample_line_layer(), sample_polygon_layer()] {
1954 let ipc = encode_layer(&layer).unwrap();
1955 let batch = decode_layer(&ipc).unwrap();
1956 let field = batch.schema().field_with_name("geometry").unwrap().clone();
1957 let meta = field.metadata();
1958 assert_eq!(
1959 meta.get(GEOARROW_EXT_KEY).map(String::as_str),
1960 Some(layer.geometry.geoarrow_name())
1961 );
1962 let crs = meta
1963 .get(GEOARROW_EXT_META_KEY)
1964 .expect("geometry field must carry ARROW:extension:metadata");
1965 assert!(crs.contains("OGC:CRS84"), "crs metadata was: {crs}");
1966 assert!(crs.contains("crs_type"), "crs metadata was: {crs}");
1967 }
1968 }
1969
1970 #[test]
1971 fn point_layer_roundtrips() {
1972 let layer = sample_point_layer();
1973 let ipc = encode_layer(&layer).unwrap();
1974 let batch = decode_layer(&ipc).unwrap();
1975
1976 assert_eq!(batch.num_rows(), 3);
1977 assert_eq!(batch.num_columns(), 6);
1979
1980 let ids = batch
1981 .column_by_name("id")
1982 .unwrap()
1983 .as_any()
1984 .downcast_ref::<UInt64Array>()
1985 .unwrap();
1986 assert_eq!(ids.values(), &[1, 2, 3]);
1987
1988 let geom = batch
1989 .column_by_name("geometry")
1990 .unwrap()
1991 .as_any()
1992 .downcast_ref::<FixedSizeListArray>()
1993 .unwrap();
1994 assert_eq!(geom.len(), 3);
1995 assert_eq!(geom.value_length(), 2);
1996
1997 let geom_field = batch.schema().field_with_name("geometry").unwrap().clone();
1999 assert_eq!(
2000 geom_field.metadata().get(GEOARROW_EXT_KEY).map(String::as_str),
2001 Some("geoarrow.point")
2002 );
2003
2004 let speed = batch
2006 .column_by_name("speed")
2007 .unwrap()
2008 .as_any()
2009 .downcast_ref::<Float64Array>()
2010 .unwrap();
2011 assert!(speed.is_null(1));
2012 assert_eq!(speed.value(0), 10.0);
2013 }
2014
2015 #[test]
2016 fn vector_property_roundtrips_as_fixed_size_list() {
2017 use arrow::array::{Float32Array, UInt8Array};
2021 let layer = ColumnarLayer {
2022 name: "surfels".to_string(),
2023 feature_ids: vec![1, 2],
2024 start_times: vec![0, 10],
2025 end_times: vec![0, 10],
2026 geometry: GeometryColumn::Point(vec![[-122.4, 37.7], [-122.5, 37.8]]),
2027 vertex_times: None,
2028 vertex_values: None,
2029 triangles: None,
2030 vertex_value_matrix: None,
2031 properties: vec![
2032 (
2033 "surfel_quat".to_string(),
2034 PropertyColumn::Vector {
2035 width: 4,
2036 elem: VectorElem::F32,
2037 values: vec![0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5, 0.5],
2038 },
2039 ),
2040 (
2041 "surfel_rgba".to_string(),
2042 PropertyColumn::Vector {
2043 width: 4,
2044 elem: VectorElem::U8,
2045 values: vec![255.0, 0.0, 0.0, 128.0, 0.0, 255.0, 0.0, 255.0],
2046 },
2047 ),
2048 ],
2049 };
2050 let ipc = encode_layer(&layer).unwrap();
2051 let batch = decode_layer(&ipc).unwrap();
2052
2053 let quat = batch
2054 .column_by_name("surfel_quat")
2055 .unwrap()
2056 .as_any()
2057 .downcast_ref::<FixedSizeListArray>()
2058 .unwrap();
2059 assert_eq!(quat.len(), 2);
2060 assert_eq!(quat.value_length(), 4);
2061 let qchild = quat
2062 .values()
2063 .as_any()
2064 .downcast_ref::<Float32Array>()
2065 .unwrap();
2066 assert_eq!(
2067 qchild.values(),
2068 &[0.0, 0.0, 0.0, 1.0, 0.5, 0.5, 0.5, 0.5]
2069 );
2070
2071 let rgba = batch
2072 .column_by_name("surfel_rgba")
2073 .unwrap()
2074 .as_any()
2075 .downcast_ref::<FixedSizeListArray>()
2076 .unwrap();
2077 assert_eq!(rgba.value_length(), 4);
2078 let cchild = rgba
2079 .values()
2080 .as_any()
2081 .downcast_ref::<UInt8Array>()
2082 .unwrap();
2083 assert_eq!(cchild.values(), &[255, 0, 0, 128, 0, 255, 0, 255]);
2084 }
2085
2086 #[test]
2087 fn vector_groups_fuse_scalar_columns_at_encode() {
2088 use arrow::array::Float32Array;
2091 let layer = ColumnarLayer {
2092 name: "surfels".to_string(),
2093 feature_ids: vec![1, 2],
2094 start_times: vec![0, 10],
2095 end_times: vec![0, 10],
2096 geometry: GeometryColumn::Point(vec![[-122.4, 37.7], [-122.5, 37.8]]),
2097 vertex_times: None,
2098 vertex_values: None,
2099 triangles: None,
2100 vertex_value_matrix: None,
2101 properties: vec![
2102 ("qx".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
2103 ("qy".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
2104 ("qz".into(), PropertyColumn::Numeric(vec![Some(0.0), Some(0.5)])),
2105 ("qw".into(), PropertyColumn::Numeric(vec![Some(1.0), Some(0.5)])),
2106 ("z".into(), PropertyColumn::Numeric(vec![Some(3.0), Some(4.0)])),
2107 ],
2108 };
2109 let cfg = EncoderConfig {
2113 vector_groups: vec![VectorGroup {
2114 name: "surfel_quat".to_string(),
2115 components: vec!["qx".into(), "qy".into(), "qz".into(), "qw".into()],
2116 elem: VectorElem::F32,
2117 }],
2118 ..EncoderConfig::default()
2119 };
2120 let ipc = encode_layer_with(&layer, &cfg).unwrap();
2121 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 let cfg = EncoderConfig {
2160 point_elevation_column: "z".to_string(),
2161 ..EncoderConfig::default()
2162 };
2163 let ipc = encode_layer_with(&layer, &cfg).unwrap();
2164 let batch = decode_layer(&ipc).unwrap();
2165
2166 let geom = batch
2168 .column_by_name("geometry")
2169 .unwrap()
2170 .as_any()
2171 .downcast_ref::<FixedSizeListArray>()
2172 .unwrap();
2173 assert_eq!(geom.value_length(), 3);
2174 let coords = geom.values().as_any().downcast_ref::<Float64Array>().unwrap();
2175 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");
2178 assert!(batch.column_by_name("speed").is_some(), "other props untouched");
2179 }
2180
2181 #[test]
2182 fn point_elevation_3d_geometry_quantizes_with_z_affine() {
2183 use arrow::array::Int32Array;
2184 let layer = ColumnarLayer {
2185 name: "cloud".into(),
2186 feature_ids: vec![1],
2187 start_times: vec![0],
2188 end_times: vec![0],
2189 geometry: GeometryColumn::Point(vec![[-122.4, 37.7]]),
2190 vertex_times: None,
2191 vertex_values: None,
2192 triangles: None,
2193 vertex_value_matrix: None,
2194 properties: vec![("z".into(), PropertyColumn::Numeric(vec![Some(5.0)]))],
2195 };
2196 let cfg = EncoderConfig {
2197 quantize_coords_m: Some(0.05),
2198 point_elevation_column: "z".to_string(),
2199 ..EncoderConfig::default()
2200 };
2201 let ipc = encode_layer_with(&layer, &cfg).unwrap();
2202 let batch = decode_layer(&ipc).unwrap();
2203
2204 let field = batch.schema().field_with_name("geometry").unwrap().clone();
2205 let affine = QuantAffine::from_json(field.metadata().get(STT_QUANT_META_KEY).unwrap()).unwrap();
2206 assert_eq!(affine.z0, Some(0.0));
2207 assert_eq!(affine.sz, Some(0.05));
2208 let geom = batch
2209 .column_by_name("geometry")
2210 .unwrap()
2211 .as_any()
2212 .downcast_ref::<FixedSizeListArray>()
2213 .unwrap();
2214 assert_eq!(geom.value_length(), 3);
2215 let coords = geom.values().as_any().downcast_ref::<Int32Array>().unwrap();
2216 assert_eq!(coords.value(2), 100);
2218 assert_eq!(affine.z0.unwrap() + coords.value(2) as f64 * affine.sz.unwrap(), 5.0);
2219 }
2220
2221 #[test]
2222 fn quantized_point_layer_roundtrips_within_precision() {
2223 let layer = sample_point_layer();
2224 let ipc = encode_layer_quantized(&layer, Some(1.0)).unwrap();
2225 let batch = decode_layer(&ipc).unwrap();
2226
2227 let geom_field = batch.schema().field_with_name("geometry").unwrap().clone();
2229 let affine = QuantAffine::from_json(
2230 geom_field
2231 .metadata()
2232 .get(STT_QUANT_META_KEY)
2233 .expect("quantized tile must carry the affine"),
2234 )
2235 .unwrap();
2236
2237 let geom = batch
2238 .column_by_name("geometry")
2239 .unwrap()
2240 .as_any()
2241 .downcast_ref::<FixedSizeListArray>()
2242 .unwrap();
2243 assert_eq!(geom.value_type(), DataType::Int32);
2244 let coords = geom
2245 .values()
2246 .as_any()
2247 .downcast_ref::<Int32Array>()
2248 .unwrap();
2249
2250 let original = [[-122.4, 37.7], [-122.5, 37.8], [-122.6, 37.9]];
2251 for (i, [lon, lat]) in original.iter().enumerate() {
2252 let rlon = affine.lon(coords.value(i * 2));
2253 let rlat = affine.lat(coords.value(i * 2 + 1));
2254 let dlon_m = (rlon - lon).abs() * M_PER_DEG_LAT * lat.to_radians().cos();
2256 let dlat_m = (rlat - lat).abs() * M_PER_DEG_LAT;
2257 assert!(dlon_m < 1.0, "lon err {dlon_m} m at point {i}");
2258 assert!(dlat_m < 1.0, "lat err {dlat_m} m at point {i}");
2259 }
2260 }
2261
2262 #[test]
2263 fn quantized_numeric_attr_roundtrips_within_precision_and_is_opt_in() {
2264 let zvals: Vec<Option<f64>> =
2268 vec![Some(1.07), Some(-2.4), Some(15.9), None, Some(40.02)];
2269 let make = || ColumnarLayer {
2270 name: "lidar".into(),
2271 feature_ids: vec![1, 2, 3, 4, 5],
2272 start_times: vec![0; 5],
2273 end_times: vec![1; 5],
2274 geometry: GeometryColumn::Point(vec![[-122.4, 37.7]; 5]),
2275 vertex_times: None,
2276 vertex_values: None,
2277 triangles: None,
2278 vertex_value_matrix: None,
2279 properties: vec![("z".into(), PropertyColumn::Numeric(zvals.clone()))],
2280 };
2281
2282 let plain =
2286 decode_layer(&encode_layer_with(&make(), &EncoderConfig::default()).unwrap()).unwrap();
2287 let zf = plain.schema().field_with_name("z").unwrap().clone();
2288 assert_eq!(zf.data_type(), &DataType::Float64);
2289 assert!(zf.metadata().get(STT_QUANT_ATTR_META_KEY).is_none());
2290
2291 let q = encode_layer_with(
2293 &make(),
2294 &EncoderConfig {
2295 quantize_attrs: HashMap::from([("z".to_string(), 0.05f64)]),
2296 ..EncoderConfig::default()
2297 },
2298 )
2299 .unwrap();
2300
2301 let batch = decode_layer(&q).unwrap();
2302 let field = batch.schema().field_with_name("z").unwrap().clone();
2303 assert_eq!(field.data_type(), &DataType::UInt16);
2305 let affine = AttrQuant::from_json(
2306 field
2307 .metadata()
2308 .get(STT_QUANT_ATTR_META_KEY)
2309 .expect("quantized attr must carry the affine"),
2310 )
2311 .unwrap();
2312
2313 let col = batch
2314 .column_by_name("z")
2315 .unwrap()
2316 .as_any()
2317 .downcast_ref::<UInt16Array>()
2318 .unwrap();
2319 for (i, want) in zvals.iter().enumerate() {
2320 match want {
2321 Some(v) => {
2322 assert!(!col.is_null(i), "row {i} should be present");
2323 let got = affine.value(col.value(i) as i64);
2324 assert!((got - v).abs() <= 0.05 / 2.0 + 1e-9, "z[{i}] {got} vs {v}");
2325 }
2326 None => assert!(col.is_null(i), "row {i} should be null"),
2327 }
2328 }
2329 }
2330
2331 #[test]
2332 fn auto_numeric_quantization_is_range_adaptive_and_opt_in() {
2333 let depth: Vec<Option<f64>> = vec![Some(0.0), Some(10.0), Some(123.4), Some(700.0)];
2337 let make = || ColumnarLayer {
2338 name: "q".into(),
2339 feature_ids: vec![1, 2, 3, 4],
2340 start_times: vec![0; 4],
2341 end_times: vec![1; 4],
2342 geometry: GeometryColumn::Point(vec![[0.0, 0.0]; 4]),
2343 vertex_times: None,
2344 vertex_values: None,
2345 triangles: None,
2346 vertex_value_matrix: None,
2347 properties: vec![("depth".into(), PropertyColumn::Numeric(depth.clone()))],
2348 };
2349
2350 let plain =
2354 decode_layer(&encode_layer_with(&make(), &EncoderConfig::default()).unwrap()).unwrap();
2355 assert_eq!(
2356 plain.schema().field_with_name("depth").unwrap().data_type(),
2357 &DataType::Float64
2358 );
2359
2360 let batch = decode_layer(
2362 &encode_layer_with(
2363 &make(),
2364 &EncoderConfig {
2365 quantize_attrs_auto: true,
2366 ..EncoderConfig::default()
2367 },
2368 )
2369 .unwrap(),
2370 )
2371 .unwrap();
2372
2373 let field = batch.schema().field_with_name("depth").unwrap().clone();
2374 assert_eq!(field.data_type(), &DataType::UInt16);
2375 let aff = AttrQuant::from_json(field.metadata().get(STT_QUANT_ATTR_META_KEY).unwrap()).unwrap();
2376 let col = batch.column_by_name("depth").unwrap().as_any().downcast_ref::<UInt16Array>().unwrap();
2377 let tol = (700.0 - 0.0) / u16::MAX as f64 / 2.0 + 1e-9;
2378 for (i, want) in depth.iter().enumerate() {
2379 let got = aff.value(col.value(i) as i64);
2380 assert!((got - want.unwrap()).abs() <= tol, "depth[{i}] {got} vs {want:?}");
2381 }
2382 assert_eq!(col.value(0), 0);
2384 assert_eq!(col.value(3), u16::MAX);
2385 }
2386
2387 #[test]
2388 fn quantization_shrinks_geometry_and_is_opt_in() {
2389 let line: Vec<[f64; 2]> = (0..400)
2392 .map(|k| [-73.95 + k as f64 * 1e-4, 40.75 + k as f64 * 7e-5])
2393 .collect();
2394 let layer = ColumnarLayer {
2395 name: "q".into(),
2396 feature_ids: vec![1],
2397 start_times: vec![0],
2398 end_times: vec![1],
2399 geometry: GeometryColumn::LineString(vec![line]),
2400 vertex_times: None,
2401 vertex_values: None,
2402 triangles: None,
2403 vertex_value_matrix: None,
2404 properties: vec![],
2405 };
2406 let plain = encode_layer_quantized(&layer, None).unwrap();
2407 let quant = encode_layer_quantized(&layer, Some(1.0)).unwrap();
2408
2409 let pb = decode_layer(&plain).unwrap();
2411 let pf = pb.schema().field_with_name("geometry").unwrap().clone();
2412 assert!(pf.metadata().get(STT_QUANT_META_KEY).is_none());
2413
2414 let qb = decode_layer(&quant).unwrap();
2416 let qf = qb.schema().field_with_name("geometry").unwrap().clone();
2417 assert!(qf.metadata().get(STT_QUANT_META_KEY).is_some());
2418
2419 assert!(
2421 quant.len() < plain.len(),
2422 "quantized {} should be smaller than f64 {}",
2423 quant.len(),
2424 plain.len()
2425 );
2426 }
2427
2428 #[test]
2429 fn line_layer_roundtrips_with_vertex_times() {
2430 let layer = sample_line_layer();
2431 let ipc = encode_layer(&layer).unwrap();
2432 let batch = decode_layer(&ipc).unwrap();
2433
2434 assert_eq!(batch.num_rows(), 2);
2435 let geom = batch
2436 .column_by_name("geometry")
2437 .unwrap()
2438 .as_any()
2439 .downcast_ref::<ListArray>()
2440 .unwrap();
2441 assert_eq!(geom.value(0).len(), 3);
2443 assert_eq!(geom.value(1).len(), 2);
2444
2445 let meta = batch.schema().metadata().clone();
2448 let origin: i64 = meta
2449 .get("stt:vertex_time_origin_ms")
2450 .expect("u16 vertex-time layers carry an origin")
2451 .parse()
2452 .unwrap();
2453 let step: u32 = meta
2454 .get("stt:vertex_time_step_ms")
2455 .expect("u16 vertex-time layers carry a step")
2456 .parse()
2457 .unwrap();
2458 assert_eq!(origin, 0);
2459 assert_eq!(step, 1);
2460
2461 let vt = batch
2462 .column_by_name("vertex_time")
2463 .unwrap()
2464 .as_any()
2465 .downcast_ref::<ListArray>()
2466 .unwrap();
2467 assert_eq!(vt.len(), 2);
2468 let first = vt.value(0);
2469 let deltas = first.as_any().downcast_ref::<arrow::array::UInt16Array>().unwrap();
2470 let absolutes: Vec<i64> = deltas
2471 .values()
2472 .iter()
2473 .map(|d| origin + (*d as i64) * step as i64)
2474 .collect();
2475 assert_eq!(absolutes, vec![0, 25, 50]);
2476 }
2477
2478 #[test]
2479 fn line_layer_roundtrips_with_vertex_values() {
2480 let layer = ColumnarLayer {
2483 name: "drift".into(),
2484 feature_ids: vec![1, 2],
2485 start_times: vec![0, 0],
2486 end_times: vec![100, 100],
2487 geometry: GeometryColumn::LineString(vec![
2488 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
2489 vec![[3.0, 3.0], [4.0, 4.0]],
2490 ]),
2491 vertex_times: None,
2492 vertex_values: Some(vec![vec![5.0, f32::NAN, 27.5], vec![12.0, 13.0]]),
2493 triangles: None,
2494 vertex_value_matrix: None,
2495 properties: vec![],
2496 };
2497 let ipc = encode_layer(&layer).unwrap();
2498 let batch = decode_layer(&ipc).unwrap();
2499
2500 let vv = batch
2501 .column_by_name("vertex_value")
2502 .expect("layers with per-vertex values carry a vertex_value column")
2503 .as_any()
2504 .downcast_ref::<ListArray>()
2505 .unwrap();
2506 assert_eq!(vv.len(), 2);
2507 let first = vv.value(0);
2508 let vals = first.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
2509 assert_eq!(vals.value(0), 5.0);
2510 assert!(vals.value(1).is_nan());
2511 assert_eq!(vals.value(2), 27.5);
2512 let second = vv.value(1);
2513 let vals2 = second.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
2514 assert_eq!(vals2.values(), &[12.0, 13.0]);
2515 }
2516
2517 #[test]
2518 fn line_layer_roundtrips_with_vertex_value_matrix() {
2519 let layer = ColumnarLayer {
2524 name: "flows".into(),
2525 feature_ids: vec![1, 2],
2526 start_times: vec![0, 0],
2527 end_times: vec![1800, 1800],
2528 geometry: GeometryColumn::LineString(vec![
2529 vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]],
2530 vec![[3.0, 3.0], [4.0, 4.0]],
2531 ]),
2532 vertex_times: None,
2533 vertex_values: None,
2534 triangles: None,
2535 vertex_value_matrix: Some(vec![
2537 vec![10.0, 11.0, 20.0, 21.0, 30.0, 31.0],
2538 vec![40.0, 41.0, 50.0, 51.0],
2539 ]),
2540 properties: vec![],
2541 };
2542 let ipc = encode_layer(&layer).unwrap();
2543 let batch = decode_layer(&ipc).unwrap();
2544
2545 let vm = batch
2546 .column_by_name("vertex_value_matrix")
2547 .expect("matrix layers carry a vertex_value_matrix column")
2548 .as_any()
2549 .downcast_ref::<ListArray>()
2550 .unwrap();
2551 assert_eq!(vm.len(), 2);
2552 let f0 = vm.value(0);
2553 let f0v = f0.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
2554 assert_eq!(f0v.values(), &[10.0, 11.0, 20.0, 21.0, 30.0, 31.0]);
2555 let f1 = vm.value(1);
2556 let f1v = f1.as_any().downcast_ref::<arrow::array::Float32Array>().unwrap();
2557 assert_eq!(f1v.values(), &[40.0, 41.0, 50.0, 51.0]);
2558
2559 assert_eq!(
2561 batch.schema().metadata().get("stt:vertex_value_buckets"),
2562 Some(&"2".to_string())
2563 );
2564 }
2565
2566 #[test]
2567 fn vertex_time_falls_back_to_int64_for_wide_spans() {
2568 let layer = ColumnarLayer {
2573 name: "edge".into(),
2574 feature_ids: vec![1],
2575 start_times: vec![0],
2576 end_times: vec![100],
2577 geometry: GeometryColumn::LineString(vec![vec![[0.0, 0.0], [1.0, 1.0]]]),
2578 vertex_times: Some(vec![vec![0, 100_000_000_000]]),
2579 vertex_values: None,
2580 triangles: None,
2581 vertex_value_matrix: None,
2582 properties: vec![],
2583 };
2584 let ipc = encode_layer(&layer).unwrap();
2585 let batch = decode_layer(&ipc).unwrap();
2586 let schema = batch.schema();
2587 let meta = schema.metadata();
2588 assert!(meta.get("stt:vertex_time_origin_ms").is_none());
2589 assert!(meta.get("stt:vertex_time_step_ms").is_none());
2590 let vt = batch
2591 .column_by_name("vertex_time")
2592 .unwrap()
2593 .as_any()
2594 .downcast_ref::<ListArray>()
2595 .unwrap();
2596 let first = vt.value(0);
2597 let absolutes = first
2598 .as_any()
2599 .downcast_ref::<Int64Array>()
2600 .expect("wide spans must keep the exact Int64 shape");
2601 assert_eq!(absolutes.values(), &[0, 100_000_000_000]);
2602 }
2603
2604 #[test]
2605 fn vertex_time_step_ceiling_is_the_u16_vs_int64_threshold() {
2606 let make = |span: i64| ColumnarLayer {
2609 name: "edge".into(),
2610 feature_ids: vec![1],
2611 start_times: vec![0],
2612 end_times: vec![100],
2613 geometry: GeometryColumn::LineString(vec![vec![[0.0, 0.0], [1.0, 1.0]]]),
2614 vertex_times: Some(vec![vec![0, span]]),
2615 vertex_values: None,
2616 triangles: None,
2617 vertex_value_matrix: None,
2618 properties: vec![],
2619 };
2620
2621 let at_ceiling = decode_layer(&encode_layer(&make(65_535_000)).unwrap()).unwrap();
2622 let schema = at_ceiling.schema();
2623 let step: u32 = schema
2624 .metadata()
2625 .get("stt:vertex_time_step_ms")
2626 .expect("span at the ceiling stays u16-delta encoded")
2627 .parse()
2628 .unwrap();
2629 assert_eq!(step, DEFAULT_VERTEX_TIME_MAX_STEP_MS);
2630
2631 let past_ceiling = decode_layer(&encode_layer(&make(65_536_000)).unwrap()).unwrap();
2632 assert!(past_ceiling
2633 .schema()
2634 .metadata()
2635 .get("stt:vertex_time_step_ms")
2636 .is_none());
2637 let vt = past_ceiling
2638 .column_by_name("vertex_time")
2639 .unwrap()
2640 .as_any()
2641 .downcast_ref::<ListArray>()
2642 .unwrap();
2643 let first = vt.value(0);
2644 let absolutes = first.as_any().downcast_ref::<Int64Array>().unwrap();
2645 assert_eq!(absolutes.values(), &[0, 65_536_000]);
2646 }
2647
2648 #[test]
2649 fn polygon_layer_roundtrips_with_rings() {
2650 let layer = sample_polygon_layer();
2651 let ipc = encode_layer(&layer).unwrap();
2652 let batch = decode_layer(&ipc).unwrap();
2653
2654 let geom = batch
2655 .column_by_name("geometry")
2656 .unwrap()
2657 .as_any()
2658 .downcast_ref::<ListArray>()
2659 .unwrap();
2660 assert_eq!(geom.len(), 1);
2661 let rings = geom.value(0);
2663 let rings = rings.as_any().downcast_ref::<ListArray>().unwrap();
2664 assert_eq!(rings.len(), 2);
2665 assert_eq!(rings.value(0).len(), 5); assert_eq!(rings.value(1).len(), 5); }
2668
2669 #[test]
2670 fn multi_layer_tile_frame_roundtrips() {
2671 let layers = vec![sample_line_layer(), sample_point_layer()];
2672 let payload = encode_tile(&layers).unwrap();
2673 let decoded = decode_tile(&payload).unwrap();
2674
2675 assert_eq!(decoded.len(), 2);
2676 assert_eq!(decoded[0].name, "tracks");
2677 assert_eq!(decoded[1].name, "points");
2678 assert_eq!(decoded[0].batch.num_rows(), 2);
2679 assert_eq!(decoded[1].batch.num_rows(), 3);
2680 assert_eq!(
2682 decoded[1]
2683 .batch
2684 .schema()
2685 .metadata()
2686 .get("stt:layer")
2687 .map(String::as_str),
2688 Some("points")
2689 );
2690 }
2691
2692 #[test]
2693 fn tessellate_polygon_emits_two_triangles_for_a_square() {
2694 let ring: Vec<Coord> = vec![
2697 [0.0, 0.0],
2698 [1.0, 0.0],
2699 [1.0, 1.0],
2700 [0.0, 1.0],
2701 [0.0, 0.0],
2702 ];
2703 let tris = tessellate_polygon(&[ring]);
2704 assert_eq!(tris.len(), 6);
2705 for &i in &tris {
2706 assert!(i < 5);
2707 }
2708 }
2709
2710 #[test]
2711 fn tessellate_polygon_handles_a_hole() {
2712 let exterior: Vec<Coord> =
2716 vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0], [0.0, 0.0]];
2717 let hole: Vec<Coord> =
2718 vec![[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0], [1.0, 1.0]];
2719 let tris = tessellate_polygon(&[exterior, hole]);
2720 assert!(tris.len() >= 6);
2721 assert_eq!(tris.len() % 3, 0);
2722 for &i in &tris {
2723 assert!(i < 10);
2724 }
2725 }
2726
2727 #[test]
2728 fn tessellate_polygon_handles_degenerate_input() {
2729 assert!(tessellate_polygon(&[]).is_empty());
2731 let degenerate: Vec<Coord> = vec![[0.0, 0.0], [1.0, 1.0]];
2733 assert!(tessellate_polygon(&[degenerate]).is_empty());
2734 }
2735
2736 #[test]
2737 fn polygon_layer_with_triangles_roundtrips() {
2738 let exterior: Vec<Coord> =
2739 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
2740 let tris = tessellate_polygon(&[exterior.clone()]);
2741 assert_eq!(tris.len(), 6);
2742 let layer = ColumnarLayer {
2743 name: "zones".into(),
2744 feature_ids: vec![42],
2745 start_times: vec![0],
2746 end_times: vec![1000],
2747 geometry: GeometryColumn::Polygon(vec![vec![exterior]]),
2748 vertex_times: None,
2749 vertex_values: None,
2750 triangles: Some(vec![tris.clone()]),
2751 vertex_value_matrix: None,
2752 properties: vec![],
2753 };
2754 let ipc = encode_layer(&layer).unwrap();
2755 let batch = decode_layer(&ipc).unwrap();
2756
2757 assert_eq!(
2759 batch
2760 .schema()
2761 .metadata()
2762 .get(TRIANGLES_METADATA_KEY)
2763 .map(String::as_str),
2764 Some("true")
2765 );
2766 let col = batch
2769 .column_by_name("triangles")
2770 .expect("triangles column present")
2771 .as_any()
2772 .downcast_ref::<ListArray>()
2773 .expect("triangles is a List");
2774 assert_eq!(col.len(), 1);
2775 let first = col.value(0);
2776 let values: &arrow::array::UInt16Array = first
2777 .as_any()
2778 .downcast_ref::<arrow::array::UInt16Array>()
2779 .expect("triangle values are UInt16 for small feature-local indices");
2780 assert_eq!(
2781 values.values().iter().map(|&v| v as u32).collect::<Vec<_>>(),
2782 tris
2783 );
2784 }
2785
2786 #[test]
2787 fn polygon_layer_with_oversized_triangle_index_falls_back_to_uint32() {
2788 let exterior: Vec<Coord> =
2792 vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
2793 let big_tris = vec![0u32, 1, 70_000];
2794 let layer = ColumnarLayer {
2795 name: "zones".into(),
2796 feature_ids: vec![42],
2797 start_times: vec![0],
2798 end_times: vec![1000],
2799 geometry: GeometryColumn::Polygon(vec![vec![exterior]]),
2800 vertex_times: None,
2801 vertex_values: None,
2802 triangles: Some(vec![big_tris.clone()]),
2803 vertex_value_matrix: None,
2804 properties: vec![],
2805 };
2806 let ipc = encode_layer(&layer).unwrap();
2807 let batch = decode_layer(&ipc).unwrap();
2808
2809 let col = batch
2810 .column_by_name("triangles")
2811 .expect("triangles column present")
2812 .as_any()
2813 .downcast_ref::<ListArray>()
2814 .expect("triangles is a List");
2815 let first = col.value(0);
2816 let values: &arrow::array::UInt32Array = first
2817 .as_any()
2818 .downcast_ref::<arrow::array::UInt32Array>()
2819 .expect("triangle values fall back to UInt32 when an index exceeds u16::MAX");
2820 assert_eq!(values.values().to_vec(), big_tris);
2821 }
2822
2823 #[test]
2824 fn polygon_layer_without_triangles_skips_the_metadata_key() {
2825 let layer = sample_polygon_layer();
2829 let ipc = encode_layer(&layer).unwrap();
2830 let batch = decode_layer(&ipc).unwrap();
2831 assert!(!batch.schema().metadata().contains_key(TRIANGLES_METADATA_KEY));
2832 assert!(batch.column_by_name("triangles").is_none());
2833 }
2834
2835 #[test]
2836 fn non_polygon_layer_drops_stray_triangles() {
2837 let mut layer = sample_point_layer();
2841 layer.triangles = Some(vec![vec![0, 1, 2]; layer.feature_ids.len()]);
2843 let ipc = encode_layer(&layer).unwrap();
2844 let batch = decode_layer(&ipc).unwrap();
2845 assert!(!batch.schema().metadata().contains_key(TRIANGLES_METADATA_KEY));
2846 assert!(batch.column_by_name("triangles").is_none());
2847 }
2848
2849 fn ipc_offsets(payload: &[u8]) -> Vec<(usize, usize)> {
2852 let raw = u16::from_le_bytes([payload[0], payload[1]]);
2853 let aligned = raw & ALIGNED_FRAME_FLAG != 0;
2854 let count = (raw & !ALIGNED_FRAME_FLAG) as usize;
2855 let mut pos = 2usize;
2856 let mut out = Vec::new();
2857 for _ in 0..count {
2858 let name_len =
2859 u16::from_le_bytes([payload[pos], payload[pos + 1]]) as usize;
2860 pos += 2 + name_len;
2861 let ipc_len = u32::from_le_bytes([
2862 payload[pos],
2863 payload[pos + 1],
2864 payload[pos + 2],
2865 payload[pos + 3],
2866 ]) as usize;
2867 pos += 4;
2868 if aligned {
2869 pos += (FRAME_ALIGN - pos % FRAME_ALIGN) % FRAME_ALIGN;
2870 }
2871 out.push((pos, ipc_len));
2872 pos += ipc_len;
2873 }
2874 out
2875 }
2876
2877 #[test]
2878 fn encoded_frames_align_every_ipc_stream_to_8_bytes() {
2879 let mut a = sample_line_layer();
2883 a.name = "x".into();
2884 let mut b = sample_point_layer();
2885 b.name = "a-longer-layer-name".into();
2886 let payload = encode_tile(&[a, b]).unwrap();
2887
2888 let raw = u16::from_le_bytes([payload[0], payload[1]]);
2889 assert_ne!(raw & ALIGNED_FRAME_FLAG, 0, "writer must set the aligned flag");
2890
2891 let offsets = ipc_offsets(&payload);
2892 assert_eq!(offsets.len(), 2);
2893 for (off, _) in &offsets {
2894 assert_eq!(off % 8, 0, "IPC stream at offset {off} is misaligned");
2895 }
2896
2897 let decoded = decode_tile(&payload).unwrap();
2899 assert_eq!(decoded[0].name, "x");
2900 assert_eq!(decoded[1].name, "a-longer-layer-name");
2901 assert_eq!(decoded[0].batch.num_rows(), 2);
2902 assert_eq!(decoded[1].batch.num_rows(), 3);
2903 }
2904
2905 #[test]
2906 fn legacy_unpadded_frames_still_decode() {
2907 let layers = vec![sample_line_layer(), sample_point_layer()];
2911 let aligned_payload = encode_tile(&layers).unwrap();
2912 let aligned = decode_tile(&aligned_payload).unwrap();
2913
2914 let mut legacy: Vec<u8> = Vec::new();
2915 legacy.extend_from_slice(&(layers.len() as u16).to_le_bytes());
2916 for ((off, len), layer) in ipc_offsets(&aligned_payload).iter().zip(&layers) {
2917 let name = layer.name.as_bytes();
2918 legacy.extend_from_slice(&(name.len() as u16).to_le_bytes());
2919 legacy.extend_from_slice(name);
2920 legacy.extend_from_slice(&(*len as u32).to_le_bytes());
2921 legacy.extend_from_slice(&aligned_payload[*off..*off + *len]);
2922 }
2923
2924 let decoded = decode_tile(&legacy).unwrap();
2925 assert_eq!(decoded.len(), aligned.len());
2926 for (l, a) in decoded.iter().zip(&aligned) {
2927 assert_eq!(l.name, a.name);
2928 assert_eq!(l.batch, a.batch);
2929 }
2930 }
2931
2932 #[test]
2933 fn truncated_tile_frame_errors_cleanly() {
2934 let payload = encode_tile(&[sample_point_layer()]).unwrap();
2935 let truncated = &payload[..payload.len() / 2];
2937 assert!(decode_tile(truncated).is_err());
2938 }
2939
2940 #[test]
2941 fn length_mismatch_is_rejected() {
2942 let mut layer = sample_point_layer();
2943 layer.start_times.pop(); assert!(encode_layer(&layer).is_err());
2945 }
2946}