1use crate::clip::ClippedSegment;
8use crate::input::ParsedFeature;
9use anyhow::Result;
10use std::collections::BTreeMap;
11use std::sync::Arc;
12use stt_core::arrow_tile::{
13 tessellate_polygon, ColumnarLayer, Coord, GeometryColumn, PropertyColumn,
14};
15use stt_core::types::GeometryType;
16
17#[derive(Debug, Clone, Default)]
27pub enum AttributeFilter {
28 #[default]
31 KeepAll,
32 Exclude(std::collections::HashSet<String>),
34 Include(std::collections::HashSet<String>),
36 ExcludeAll,
38}
39
40impl AttributeFilter {
41 pub fn keeps(&self, name: &str) -> bool {
43 match self {
44 AttributeFilter::KeepAll => true,
45 AttributeFilter::Exclude(set) => !set.contains(name),
46 AttributeFilter::Include(set) => set.contains(name),
47 AttributeFilter::ExcludeAll => false,
48 }
49 }
50
51 pub fn is_keep_all(&self) -> bool {
53 matches!(self, AttributeFilter::KeepAll)
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum PropertyKind {
62 Numeric,
64 Categorical,
66}
67
68pub type PropertyTypes = BTreeMap<String, PropertyKind>;
71
72#[derive(Debug, Clone, Default)]
75pub struct ColumnarOptions {
76 pub pre_tessellate: bool,
80 pub attribute_filter: AttributeFilter,
83 pub property_types: Arc<PropertyTypes>,
91}
92
93pub fn build_layers_from_features(
99 features: &[&ParsedFeature],
100 layer_name: &str,
101) -> Result<Vec<ColumnarLayer>> {
102 build_layers_from_features_with(features, layer_name, ColumnarOptions::default())
103}
104
105pub fn build_layers_from_features_with(
107 features: &[&ParsedFeature],
108 layer_name: &str,
109 opts: ColumnarOptions,
110) -> Result<Vec<ColumnarLayer>> {
111 if features.is_empty() {
112 return Ok(vec![]);
113 }
114
115 let mut points: Vec<&ParsedFeature> = Vec::new();
117 let mut lines: Vec<&ParsedFeature> = Vec::new();
118 let mut polygons: Vec<&ParsedFeature> = Vec::new();
119 for f in features {
120 match determine_geometry_type(f) {
121 Ok(GeometryType::Point) => points.push(f),
122 Ok(GeometryType::LineString) => lines.push(f),
123 Ok(GeometryType::Polygon) => polygons.push(f),
124 Err(e) => tracing::warn!("skipping feature with no geometry: {e}"),
125 }
126 }
127
128 let mut layers = Vec::new();
129 let kinds_present =
132 [!points.is_empty(), !lines.is_empty(), !polygons.is_empty()]
133 .iter()
134 .filter(|p| **p)
135 .count();
136 let name_for = |kind: &str| -> String {
137 if kinds_present <= 1 {
138 layer_name.to_string()
139 } else {
140 format!("{layer_name}_{kind}")
141 }
142 };
143
144 if !points.is_empty() {
145 layers.push(build_point_layer(&points, name_for("points"), &opts)?);
146 }
147 if !lines.is_empty() {
148 layers.push(build_line_layer(&lines, name_for("lines"), &opts)?);
149 }
150 if !polygons.is_empty() {
151 layers.push(build_polygon_layer(&polygons, name_for("polygons"), &opts)?);
152 }
153 Ok(layers)
154}
155
156pub fn build_layer_from_segments(
159 segments: &[&ClippedSegment],
160 layer_name: &str,
161 opts: &ColumnarOptions,
162) -> Result<ColumnarLayer> {
163 let n = segments.len();
164 let mut feature_ids = Vec::with_capacity(n);
165 let mut start_times = Vec::with_capacity(n);
166 let mut end_times = Vec::with_capacity(n);
167 let mut geometry: Vec<Vec<Coord>> = Vec::with_capacity(n);
168 let mut vertex_times: Vec<Vec<i64>> = Vec::with_capacity(n);
169 let mut vertex_values: Vec<Vec<f32>> = Vec::with_capacity(n);
170 let mut vertex_value_matrix: Vec<Vec<f32>> = Vec::with_capacity(n);
171 let mut any_values = false;
172 let mut any_matrix = false;
173
174 let mut props = PropertyAccumulator::new(
175 opts.attribute_filter.clone(),
176 Arc::clone(&opts.property_types),
177 );
178
179 for seg in segments {
180 feature_ids.push(segment_feature_id(seg));
181 start_times.push(seg.start_time as i64);
182 end_times.push(seg.end_time as i64);
183
184 let coords: Vec<Coord> = seg.coordinates.iter().map(|(x, y, _alt)| [*x, *y]).collect();
185
186 let mut times: Vec<i64> = Vec::with_capacity(coords.len());
189 for i in 0..coords.len() {
190 let t = seg.timestamps.get(i).copied().unwrap_or(seg.start_time);
191 times.push(t as i64);
192 }
193
194 if !seg.vertex_values.is_empty() {
197 any_values = true;
198 }
199 let mut vals: Vec<f32> = Vec::with_capacity(coords.len());
200 for i in 0..coords.len() {
201 vals.push(seg.vertex_values.get(i).copied().unwrap_or(f32::NAN));
202 }
203
204 if !seg.vertex_value_matrix.is_empty() {
208 any_matrix = true;
209 let nb = seg.vertex_value_matrix[0].len();
210 let mut flat = Vec::with_capacity(coords.len() * nb);
211 for row in &seg.vertex_value_matrix {
212 flat.extend_from_slice(row);
213 }
214 vertex_value_matrix.push(flat);
215 } else {
216 vertex_value_matrix.push(Vec::new());
217 }
218
219 geometry.push(coords);
220 vertex_times.push(times);
221 vertex_values.push(vals);
222
223 props.observe(seg.properties.as_deref());
224 }
225 for seg in segments {
227 props.push_row(seg.properties.as_deref());
228 }
229
230 Ok(ColumnarLayer {
231 name: layer_name.to_string(),
232 feature_ids,
233 start_times,
234 end_times,
235 geometry: GeometryColumn::LineString(geometry),
236 vertex_times: (!any_matrix).then_some(vertex_times),
240 vertex_values: any_values.then_some(vertex_values),
242 triangles: None,
243 vertex_value_matrix: any_matrix.then_some(vertex_value_matrix),
244 properties: props.finish(),
245 })
246}
247
248fn build_point_layer(
253 features: &[&ParsedFeature],
254 name: String,
255 opts: &ColumnarOptions,
256) -> Result<ColumnarLayer> {
257 let (mut ids, start, end, props) = common_columns(features, opts);
258 for (i, f) in features.iter().enumerate() {
269 if f.geojson.id.is_none() {
270 ids[i] = i as u64;
271 }
272 }
273 let geometry: Vec<Coord> = features.iter().map(|f| [f.lon, f.lat]).collect();
274 Ok(ColumnarLayer {
275 name,
276 feature_ids: ids,
277 start_times: start,
278 end_times: end,
279 geometry: GeometryColumn::Point(geometry),
280 vertex_times: None,
281 vertex_values: None,
282 triangles: None,
283 vertex_value_matrix: None,
284 properties: props,
285 })
286}
287
288fn build_line_layer(
289 features: &[&ParsedFeature],
290 name: String,
291 opts: &ColumnarOptions,
292) -> Result<ColumnarLayer> {
293 let (ids, start, end, props) = common_columns(features, opts);
294
295 let mut geometry: Vec<Vec<Coord>> = Vec::with_capacity(features.len());
296 let mut vertex_times: Vec<Vec<i64>> = Vec::with_capacity(features.len());
297 let mut vertex_values: Vec<Vec<f32>> = Vec::with_capacity(features.len());
298 let mut vertex_value_matrix: Vec<Vec<f32>> = Vec::with_capacity(features.len());
299 let mut any_duration = false;
300 let mut any_values = false;
301 let mut any_matrix = false;
302 let mut length_mismatch_warned = false;
303
304 for f in features {
305 let coords = extract_line_coords(f)?;
306 let times = if let Some(supplied) = f.vertex_timestamps.as_ref() {
315 if supplied.len() == coords.len() {
316 any_duration = true;
317 supplied.iter().map(|&t| t as i64).collect()
318 } else {
319 if !length_mismatch_warned {
320 tracing::warn!(
321 "vertex_timestamps length {} != coord count {} for a line \
322 feature; falling back to distance interpolation (further \
323 mismatches in this build will be silent)",
324 supplied.len(),
325 coords.len()
326 );
327 length_mismatch_warned = true;
328 }
329 if let Some(end_ts) = f.end_timestamp {
330 any_duration = true;
331 interpolate_vertex_times(&coords, f.timestamp, end_ts)
332 } else {
333 vec![f.timestamp as i64; coords.len()]
334 }
335 }
336 } else if let Some(end_ts) = f.end_timestamp {
337 any_duration = true;
338 interpolate_vertex_times(&coords, f.timestamp, end_ts)
339 } else {
340 vec![f.timestamp as i64; coords.len()]
341 };
342 let vals: Vec<f32> = match f.vertex_values.as_ref() {
345 Some(supplied) if supplied.len() == coords.len() => {
346 any_values = true;
347 supplied.clone()
348 }
349 _ => vec![f32::NAN; coords.len()],
350 };
351 let matrix: Vec<f32> = match f.vertex_value_matrix.as_ref() {
354 Some(m) if !m.is_empty() && m.len() % coords.len() == 0 => {
355 any_matrix = true;
356 m.clone()
357 }
358 _ => Vec::new(),
359 };
360
361 geometry.push(coords);
362 vertex_times.push(times);
363 vertex_values.push(vals);
364 vertex_value_matrix.push(matrix);
365 }
366
367 Ok(ColumnarLayer {
368 name,
369 feature_ids: ids,
370 start_times: start,
371 end_times: end,
372 geometry: GeometryColumn::LineString(geometry),
373 vertex_times: (any_duration && !any_matrix).then_some(vertex_times),
380 vertex_values: any_values.then_some(vertex_values),
382 triangles: None,
383 vertex_value_matrix: any_matrix.then_some(vertex_value_matrix),
384 properties: props,
385 })
386}
387
388fn build_polygon_layer(
389 features: &[&ParsedFeature],
390 name: String,
391 opts: &ColumnarOptions,
392) -> Result<ColumnarLayer> {
393 let (ids, start, end, props) = common_columns(features, opts);
394 let mut geometry: Vec<Vec<Vec<Coord>>> = Vec::with_capacity(features.len());
395 for f in features {
396 geometry.push(extract_polygon_rings(f)?);
397 }
398 let needs_triangles =
413 opts.pre_tessellate || geometry.iter().any(|rings| rings.len() > 1);
414 let triangles = if needs_triangles {
415 let mut tris: Vec<Vec<u32>> = Vec::with_capacity(geometry.len());
416 for rings in &geometry {
417 tris.push(tessellate_polygon(rings));
418 }
419 Some(tris)
420 } else {
421 None
422 };
423 Ok(ColumnarLayer {
424 name,
425 feature_ids: ids,
426 start_times: start,
427 end_times: end,
428 geometry: GeometryColumn::Polygon(geometry),
429 vertex_times: None,
430 vertex_values: None,
431 triangles,
432 vertex_value_matrix: None,
433 properties: props,
434 })
435}
436
437fn common_columns(
439 features: &[&ParsedFeature],
440 opts: &ColumnarOptions,
441) -> (Vec<u64>, Vec<i64>, Vec<i64>, Vec<(String, PropertyColumn)>) {
442 let mut ids = Vec::with_capacity(features.len());
443 let mut start = Vec::with_capacity(features.len());
444 let mut end = Vec::with_capacity(features.len());
445 let mut props = PropertyAccumulator::new(
446 opts.attribute_filter.clone(),
447 Arc::clone(&opts.property_types),
448 );
449
450 for f in features {
451 ids.push(determine_feature_id(f));
452 start.push(f.timestamp as i64);
453 end.push(f.end_timestamp.unwrap_or(f.timestamp) as i64);
454 props.observe(f.shared_properties.as_deref());
455 }
456 for f in features {
457 props.push_row(f.shared_properties.as_deref());
458 }
459 (ids, start, end, props.finish())
460}
461
462struct PropertyAccumulator {
470 seen: BTreeMap<String, KeyKind>,
472 numeric: BTreeMap<String, Vec<Option<f64>>>,
474 categorical: BTreeMap<String, Vec<Option<String>>>,
476 sealed: bool,
479 filter: AttributeFilter,
484 declared: Arc<PropertyTypes>,
488}
489
490#[derive(Default)]
492struct KeyKind {
493 has_number: bool,
495 has_numeric_string: bool,
497 has_other: bool,
499}
500
501fn value_as_f64(v: &serde_json::Value) -> Option<f64> {
506 match v {
507 serde_json::Value::Number(_) => v.as_f64(),
508 serde_json::Value::String(s) => s.trim().parse::<f64>().ok().filter(|f| f.is_finite()),
509 _ => None,
510 }
511}
512
513impl PropertyAccumulator {
514 fn new(filter: AttributeFilter, declared: Arc<PropertyTypes>) -> Self {
518 Self {
519 seen: BTreeMap::new(),
520 numeric: BTreeMap::new(),
521 categorical: BTreeMap::new(),
522 sealed: false,
523 filter,
524 declared,
525 }
526 }
527
528 fn observe(&mut self, props: Option<&serde_json::Map<String, serde_json::Value>>) {
530 if self.sealed {
531 return;
532 }
533 let Some(props) = props else { return };
534 for (key, value) in props {
535 if value.is_null() {
536 continue;
537 }
538 if !self.filter.keeps(key) {
542 continue;
543 }
544 let kind = self.seen.entry(key.clone()).or_default();
545 if value.is_number() {
546 kind.has_number = true;
547 } else if let Some(s) = value.as_str() {
548 if s.trim().parse::<f64>().map(|f| f.is_finite()).unwrap_or(false) {
549 kind.has_numeric_string = true;
550 } else {
551 kind.has_other = true;
552 }
553 } else {
554 kind.has_other = true;
557 }
558 }
559 }
560
561 fn seal(&mut self) {
569 if self.sealed {
570 return;
571 }
572 self.sealed = true;
573 let declared = Arc::clone(&self.declared);
574 for (key, kind) in declared.iter() {
575 if !self.filter.keeps(key) {
576 continue;
577 }
578 match kind {
579 PropertyKind::Numeric => {
580 self.numeric.insert(key.clone(), Vec::new());
581 }
582 PropertyKind::Categorical => {
583 self.categorical.insert(key.clone(), Vec::new());
584 }
585 }
586 }
587 for (key, kind) in &self.seen {
588 if self.numeric.contains_key(key) || self.categorical.contains_key(key) {
589 continue; }
591 let is_numeric = (kind.has_number || kind.has_numeric_string) && !kind.has_other;
592 if is_numeric {
593 self.numeric.insert(key.clone(), Vec::new());
594 } else {
595 self.categorical.insert(key.clone(), Vec::new());
596 }
597 }
598 }
599
600 fn push_row(&mut self, props: Option<&serde_json::Map<String, serde_json::Value>>) {
602 if !self.sealed {
603 self.seal();
604 }
605 for (key, col) in self.numeric.iter_mut() {
606 let v = props.and_then(|p| p.get(key)).and_then(value_as_f64);
607 col.push(v);
608 }
609 for (key, col) in self.categorical.iter_mut() {
610 let v = props.and_then(|p| p.get(key)).and_then(|v| match v {
611 serde_json::Value::String(s) => Some(s.clone()),
612 serde_json::Value::Bool(b) => Some(b.to_string()),
613 serde_json::Value::Number(n) => Some(n.to_string()),
614 _ => None,
615 });
616 col.push(v);
617 }
618 }
619
620 fn finish(self) -> Vec<(String, PropertyColumn)> {
621 let mut out = Vec::new();
622 for (name, values) in self.numeric {
623 out.push((name, PropertyColumn::Numeric(values)));
624 }
625 for (name, values) in self.categorical {
626 out.push((name, PropertyColumn::Categorical(values)));
627 }
628 out
629 }
630}
631
632pub fn determine_geometry_type(feature: &ParsedFeature) -> Result<GeometryType> {
638 use geojson::Value as GeomValue;
639 let geom = feature
640 .geojson
641 .geometry
642 .as_ref()
643 .ok_or_else(|| anyhow::anyhow!("feature has no geometry"))?;
644 Ok(match &geom.value {
645 GeomValue::Point(_) | GeomValue::MultiPoint(_) => GeometryType::Point,
646 GeomValue::LineString(_) | GeomValue::MultiLineString(_) => GeometryType::LineString,
647 GeomValue::Polygon(_) | GeomValue::MultiPolygon(_) => GeometryType::Polygon,
648 GeomValue::GeometryCollection(c) => match c.first().map(|g| &g.value) {
649 Some(GeomValue::Point(_)) | Some(GeomValue::MultiPoint(_)) => GeometryType::Point,
650 Some(GeomValue::LineString(_)) | Some(GeomValue::MultiLineString(_)) => {
651 GeometryType::LineString
652 }
653 _ => GeometryType::Polygon,
654 },
655 })
656}
657
658fn extract_line_coords(feature: &ParsedFeature) -> Result<Vec<Coord>> {
660 use geojson::Value as GeomValue;
661 let geom = feature
662 .geojson
663 .geometry
664 .as_ref()
665 .ok_or_else(|| anyhow::anyhow!("feature has no geometry"))?;
666 let coords: Vec<Coord> = match &geom.value {
667 GeomValue::LineString(pts) => pts.iter().filter(|c| c.len() >= 2).map(|c| [c[0], c[1]]).collect(),
668 GeomValue::MultiLineString(lines) => lines
669 .iter()
670 .flatten()
671 .filter(|c| c.len() >= 2)
672 .map(|c| [c[0], c[1]])
673 .collect(),
674 _ => vec![[feature.lon, feature.lat]],
675 };
676 if coords.is_empty() {
677 Ok(vec![[feature.lon, feature.lat]])
678 } else {
679 Ok(coords)
680 }
681}
682
683fn extract_polygon_rings(feature: &ParsedFeature) -> Result<Vec<Vec<Coord>>> {
686 use geojson::Value as GeomValue;
687 let geom = feature
688 .geojson
689 .geometry
690 .as_ref()
691 .ok_or_else(|| anyhow::anyhow!("feature has no geometry"))?;
692 let to_ring = |ring: &Vec<Vec<f64>>| -> Vec<Coord> {
693 ring.iter().filter(|c| c.len() >= 2).map(|c| [c[0], c[1]]).collect()
694 };
695 let rings: Vec<Vec<Coord>> = match &geom.value {
696 GeomValue::Polygon(rings) => rings
697 .iter()
698 .map(to_ring)
699 .filter(|r| r.len() >= 4)
700 .collect(),
701 GeomValue::MultiPolygon(polys) => polys
702 .iter()
703 .flat_map(|p| p.iter().map(to_ring))
704 .filter(|r| r.len() >= 4)
705 .collect(),
706 _ => vec![],
707 };
708 if rings.is_empty() {
709 Ok(vec![vec![[feature.lon, feature.lat]]])
711 } else {
712 Ok(rings)
713 }
714}
715
716fn interpolate_vertex_times(coords: &[Coord], start: u64, end: u64) -> Vec<i64> {
718 let n = coords.len();
719 if n == 0 {
720 return vec![];
721 }
722 if n == 1 {
723 return vec![start as i64];
724 }
725 let mut cumulative = vec![0.0f64; n];
726 for i in 1..n {
727 let [lon1, lat1] = coords[i - 1];
728 let [lon2, lat2] = coords[i];
729 cumulative[i] = cumulative[i - 1] + haversine_distance(lat1, lon1, lat2, lon2);
730 }
731 let total = cumulative[n - 1];
732 let duration = end as f64 - start as f64;
733 if total <= 0.0 {
734 return vec![start as i64; n];
735 }
736 cumulative
737 .iter()
738 .map(|d| start as i64 + (d / total * duration) as i64)
739 .collect()
740}
741
742fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
744 const EARTH_RADIUS: f64 = 6_371_000.0;
745 let dlat = (lat2 - lat1).to_radians();
746 let dlon = (lon2 - lon1).to_radians();
747 let a = (dlat / 2.0).sin().powi(2)
748 + lat1.to_radians().cos() * lat2.to_radians().cos() * (dlon / 2.0).sin().powi(2);
749 EARTH_RADIUS * 2.0 * a.sqrt().asin()
750}
751
752fn fnv1a_64(bytes: &[u8]) -> u64 {
766 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
767 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
768 let mut hash = FNV_OFFSET_BASIS;
769 for &b in bytes {
770 hash ^= b as u64;
771 hash = hash.wrapping_mul(FNV_PRIME);
772 }
773 hash
774}
775
776fn fnv1a_64_fields(fields: &[u64]) -> u64 {
778 let mut bytes = Vec::with_capacity(fields.len() * 8);
779 for f in fields {
780 bytes.extend_from_slice(&f.to_le_bytes());
781 }
782 fnv1a_64(&bytes)
783}
784
785fn determine_feature_id(feature: &ParsedFeature) -> u64 {
787 use geojson::feature::Id;
788
789 if let Some(id) = &feature.geojson.id {
790 match id {
791 Id::Number(num) => {
792 if let Some(v) = num.as_u64() {
793 return v;
794 }
795 if let Some(v) = num.as_i64() {
796 return v as u64;
797 }
798 }
799 Id::String(s) => {
800 return fnv1a_64(s.as_bytes());
801 }
802 }
803 }
804 fnv1a_64_fields(&[
805 feature.timestamp,
806 feature.lon.to_bits(),
807 feature.lat.to_bits(),
808 ])
809}
810
811fn segment_feature_id(segment: &ClippedSegment) -> u64 {
813 use geojson::feature::Id;
814
815 if let Some(id) = &segment.feature_id {
816 match id {
817 Id::Number(num) => {
818 if let Some(v) = num.as_u64() {
819 return v;
820 }
821 if let Some(v) = num.as_i64() {
822 return v as u64;
823 }
824 }
825 Id::String(s) => {
826 return fnv1a_64(s.as_bytes());
827 }
828 }
829 }
830 match segment.coordinates.first() {
831 Some((lon, lat, _)) => {
832 fnv1a_64_fields(&[segment.start_time, lon.to_bits(), lat.to_bits()])
833 }
834 None => fnv1a_64_fields(&[segment.start_time]),
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841 use geojson::{Feature, Geometry, Value as GeomValue};
842 use serde_json::json;
843
844 fn point_feature(lon: f64, lat: f64, props: serde_json::Value) -> ParsedFeature {
845 ParsedFeature {
846 geojson: Feature {
847 bbox: None,
848 geometry: Some(Geometry::new(GeomValue::Point(vec![lon, lat]))),
849 id: None,
850 properties: None,
851 foreign_members: None,
852 },
853 shared_properties: props
855 .as_object()
856 .filter(|m| !m.is_empty())
857 .map(|m| std::sync::Arc::new(m.clone())),
858 timestamp: 1000,
859 end_timestamp: None,
860 vertex_timestamps: None,
861 vertex_values: None,
862 vertex_value_matrix: None,
863 lon,
864 lat,
865 }
866 }
867
868 fn line_feature(coords: Vec<[f64; 2]>, start: u64, end: Option<u64>) -> ParsedFeature {
869 let pts: Vec<Vec<f64>> = coords.iter().map(|c| vec![c[0], c[1]]).collect();
870 ParsedFeature {
871 geojson: Feature {
872 bbox: None,
873 geometry: Some(Geometry::new(GeomValue::LineString(pts))),
874 id: None,
875 properties: None,
876 foreign_members: None,
877 },
878 shared_properties: None,
879 timestamp: start,
880 end_timestamp: end,
881 vertex_timestamps: None,
882 vertex_values: None,
883 vertex_value_matrix: None,
884 lon: coords[0][0],
885 lat: coords[0][1],
886 }
887 }
888
889 #[test]
890 fn point_features_become_one_layer() {
891 let f1 = point_feature(-122.4, 37.7, json!({ "speed": 10.0, "kind": "car" }));
892 let f2 = point_feature(-122.5, 37.8, json!({ "speed": 20.0 }));
893 let refs = vec![&f1, &f2];
894 let layers = build_layers_from_features(&refs, "default").unwrap();
895 assert_eq!(layers.len(), 1);
896 assert_eq!(layers[0].feature_count(), 2);
897 let kind = layers[0]
899 .properties
900 .iter()
901 .find(|(n, _)| n == "kind")
902 .expect("kind column");
903 match &kind.1 {
904 PropertyColumn::Categorical(v) => {
905 assert_eq!(v[0].as_deref(), Some("car"));
906 assert_eq!(v[1], None);
907 }
908 _ => panic!("kind should be categorical"),
909 }
910 }
911
912 #[test]
913 fn numeric_string_and_boolean_properties_are_classified() {
914 let f1 = point_feature(
919 -122.4,
920 37.7,
921 json!({ "altitude": 1000.0, "label": "alpha", "active": true }),
922 );
923 let f2 = point_feature(
924 -122.5,
925 37.8,
926 json!({ "altitude": 2000.0, "label": "beta", "active": false }),
927 );
928 let refs = vec![&f1, &f2];
929 let layers = build_layers_from_features(&refs, "default").unwrap();
930 let col = |name: &str| {
931 layers[0]
932 .properties
933 .iter()
934 .find(|(n, _)| n == name)
935 .map(|(_, c)| c)
936 };
937
938 match col("altitude").expect("altitude column") {
939 PropertyColumn::Numeric(v) => {
940 assert_eq!(v[0], Some(1000.0));
941 assert_eq!(v[1], Some(2000.0));
942 }
943 _ => panic!("altitude should be numeric"),
944 }
945 match col("label").expect("label column") {
946 PropertyColumn::Categorical(v) => {
947 assert_eq!(v[0].as_deref(), Some("alpha"));
948 assert_eq!(v[1].as_deref(), Some("beta"));
949 }
950 _ => panic!("label should be categorical"),
951 }
952 match col("active").expect("boolean column must be present, not dropped") {
955 PropertyColumn::Categorical(v) => {
956 assert_eq!(v[0].as_deref(), Some("true"));
957 assert_eq!(v[1].as_deref(), Some("false"));
958 }
959 _ => panic!("boolean should be carried as categorical"),
960 }
961 }
962
963 #[test]
968 fn numeric_strings_are_promoted_to_numeric() {
969 let f1 = point_feature(
970 -122.4,
971 37.7,
972 json!({ "altitude": "1000.0", "code": "A12", "mixed": "5" }),
973 );
974 let f2 = point_feature(
975 -122.5,
976 37.8,
977 json!({ "altitude": "2000", "code": "B7", "mixed": "n/a" }),
978 );
979 let layers = build_layers_from_features(&[&f1, &f2], "default").unwrap();
980 let col = |name: &str| {
981 layers[0]
982 .properties
983 .iter()
984 .find(|(n, _)| n == name)
985 .map(|(_, c)| c)
986 };
987
988 match col("altitude").expect("altitude column") {
990 PropertyColumn::Numeric(v) => {
991 assert_eq!(v[0], Some(1000.0));
992 assert_eq!(v[1], Some(2000.0));
993 }
994 _ => panic!("string-encoded numbers should promote to numeric"),
995 }
996 match col("code").expect("code column") {
998 PropertyColumn::Categorical(v) => {
999 assert_eq!(v[0].as_deref(), Some("A12"));
1000 assert_eq!(v[1].as_deref(), Some("B7"));
1001 }
1002 _ => panic!("non-numeric strings should stay categorical"),
1003 }
1004 match col("mixed").expect("mixed column") {
1007 PropertyColumn::Categorical(v) => {
1008 assert_eq!(v[0].as_deref(), Some("5"));
1009 assert_eq!(v[1].as_deref(), Some("n/a"));
1010 }
1011 _ => panic!("mixed numeric/non-numeric column should stay categorical"),
1012 }
1013 }
1014
1015 #[test]
1018 fn exclude_drops_only_named_property() {
1019 let f1 = point_feature(-122.4, 37.7, json!({ "speed": 10.0, "kind": "car", "name": "a" }));
1020 let f2 = point_feature(-122.5, 37.8, json!({ "speed": 20.0, "kind": "bus", "name": "b" }));
1021 let opts = ColumnarOptions {
1022 attribute_filter: AttributeFilter::Exclude(
1023 ["kind".to_string()].into_iter().collect(),
1024 ),
1025 ..Default::default()
1026 };
1027 let layers =
1028 build_layers_from_features_with(&[&f1, &f2], "default", opts).unwrap();
1029 let names: Vec<&str> = layers[0].properties.iter().map(|(n, _)| n.as_str()).collect();
1030 assert!(!names.contains(&"kind"), "excluded property must be gone");
1031 assert!(names.contains(&"speed"));
1032 assert!(names.contains(&"name"));
1033 assert_eq!(layers[0].feature_count(), 2);
1035 assert_eq!(layers[0].start_times.len(), 2);
1036 assert_eq!(layers[0].geometry.len(), 2);
1037 }
1038
1039 #[test]
1041 fn include_keeps_only_named_properties() {
1042 let f1 = point_feature(-122.4, 37.7, json!({ "speed": 10.0, "kind": "car", "name": "a" }));
1043 let f2 = point_feature(-122.5, 37.8, json!({ "speed": 20.0, "kind": "bus", "name": "b" }));
1044 let opts = ColumnarOptions {
1045 attribute_filter: AttributeFilter::Include(
1046 ["speed".to_string()].into_iter().collect(),
1047 ),
1048 ..Default::default()
1049 };
1050 let layers =
1051 build_layers_from_features_with(&[&f1, &f2], "default", opts).unwrap();
1052 let names: Vec<&str> = layers[0].properties.iter().map(|(n, _)| n.as_str()).collect();
1053 assert_eq!(names, vec!["speed"], "only the included property survives");
1054 assert_eq!(layers[0].feature_count(), 2);
1056 assert!(!layers[0].start_times.is_empty());
1057 assert!(!layers[0].end_times.is_empty());
1058 }
1059
1060 #[test]
1062 fn exclude_all_drops_every_user_property() {
1063 let f1 = point_feature(-122.4, 37.7, json!({ "speed": 10.0, "kind": "car" }));
1064 let opts = ColumnarOptions {
1065 attribute_filter: AttributeFilter::ExcludeAll,
1066 ..Default::default()
1067 };
1068 let layers =
1069 build_layers_from_features_with(&[&f1], "default", opts).unwrap();
1070 assert!(layers[0].properties.is_empty(), "no user property survives");
1071 assert_eq!(layers[0].feature_count(), 1);
1073 assert_eq!(layers[0].geometry.len(), 1);
1074 }
1075
1076 #[test]
1082 fn declared_property_kinds_pin_schema_for_all_null_tiles() {
1083 let with_val = point_feature(-122.4, 37.7, json!({ "sog": 3.5, "class": "cargo" }));
1086 let all_null = point_feature(10.0, 50.0, json!({ "class": "tanker" }));
1087
1088 let declared: PropertyTypes = [
1089 ("sog".to_string(), PropertyKind::Numeric),
1090 ("class".to_string(), PropertyKind::Categorical),
1091 ]
1092 .into_iter()
1093 .collect();
1094 let opts = ColumnarOptions {
1095 property_types: Arc::new(declared),
1096 ..Default::default()
1097 };
1098
1099 let tile_b =
1101 build_layers_from_features_with(&[&all_null], "default", opts.clone()).unwrap();
1102 let names_b: Vec<&str> =
1103 tile_b[0].properties.iter().map(|(n, _)| n.as_str()).collect();
1104 assert_eq!(names_b, vec!["sog", "class"], "declared columns always present");
1106 match &tile_b[0].properties.iter().find(|(n, _)| n == "sog").unwrap().1 {
1107 PropertyColumn::Numeric(v) => assert_eq!(v, &vec![None]),
1108 other => panic!("declared-numeric sog must stay Numeric, got {other:?}"),
1109 }
1110
1111 let tile_a =
1113 build_layers_from_features_with(&[&with_val], "default", opts).unwrap();
1114 let names_a: Vec<&str> =
1115 tile_a[0].properties.iter().map(|(n, _)| n.as_str()).collect();
1116 assert_eq!(names_a, names_b, "schema identical across tiles");
1117 match &tile_a[0].properties.iter().find(|(n, _)| n == "sog").unwrap().1 {
1118 PropertyColumn::Numeric(v) => assert_eq!(v, &vec![Some(3.5)]),
1119 other => panic!("expected Numeric sog, got {other:?}"),
1120 }
1121
1122 let numeric_string = point_feature(0.0, 0.0, json!({ "class": "42" }));
1125 let opts2 = ColumnarOptions {
1126 property_types: Arc::new(
1127 [("class".to_string(), PropertyKind::Categorical)].into_iter().collect(),
1128 ),
1129 ..Default::default()
1130 };
1131 let tile_c =
1132 build_layers_from_features_with(&[&numeric_string], "default", opts2).unwrap();
1133 match &tile_c[0].properties.iter().find(|(n, _)| n == "class").unwrap().1 {
1134 PropertyColumn::Categorical(v) => assert_eq!(v, &vec![Some("42".to_string())]),
1135 other => panic!("declared-categorical must stay Categorical, got {other:?}"),
1136 }
1137 }
1138
1139 #[test]
1141 fn segment_layer_honours_attribute_filter() {
1142 use crate::clip::ClippedSegment;
1143 let props = json!({ "road": "main", "lanes": 4 })
1144 .as_object()
1145 .cloned()
1146 .map(std::sync::Arc::new);
1147 let seg = ClippedSegment {
1148 tile_x: 0,
1149 tile_y: 0,
1150 zoom: 10,
1151 coordinates: vec![(0.0, 0.0, 0.0), (1.0, 1.0, 0.0)],
1152 timestamps: vec![1000, 2000],
1153 vertex_values: vec![],
1154 vertex_value_matrix: vec![],
1155 start_time: 1000,
1156 end_time: 2000,
1157 properties: props,
1158 feature_id: None,
1159 };
1160 let layer = build_layer_from_segments(
1161 &[&seg],
1162 "tracks",
1163 &ColumnarOptions {
1164 attribute_filter: AttributeFilter::Include(
1165 ["road".to_string()].into_iter().collect(),
1166 ),
1167 ..Default::default()
1168 },
1169 )
1170 .unwrap();
1171 let names: Vec<&str> = layer.properties.iter().map(|(n, _)| n.as_str()).collect();
1172 assert_eq!(names, vec!["road"]);
1173 assert!(layer.vertex_times.is_some());
1175 }
1176
1177 #[test]
1178 fn mixed_geometry_types_split_into_separate_layers() {
1179 let pt = point_feature(0.0, 0.0, json!({}));
1180 let line = line_feature(vec![[0.0, 0.0], [1.0, 1.0]], 1000, None);
1181 let refs = vec![&pt, &line];
1182 let layers = build_layers_from_features(&refs, "default").unwrap();
1183 assert_eq!(layers.len(), 2);
1184 let names: Vec<&str> = layers.iter().map(|l| l.name.as_str()).collect();
1186 assert!(names.contains(&"default_points"));
1187 assert!(names.contains(&"default_lines"));
1188 }
1189
1190 #[test]
1191 fn line_with_duration_gets_interpolated_vertex_times() {
1192 let line = line_feature(
1193 vec![[0.0, 0.0], [0.0, 1.0], [0.0, 2.0]],
1194 1000,
1195 Some(3000),
1196 );
1197 let refs = vec![&line];
1198 let layers = build_layers_from_features(&refs, "default").unwrap();
1199 let vt = layers[0].vertex_times.as_ref().expect("vertex times present");
1200 assert_eq!(vt[0].len(), 3);
1201 assert_eq!(vt[0][0], 1000);
1203 assert_eq!(vt[0][2], 3000);
1204 assert!((vt[0][1] - 2000).abs() <= 1);
1205 }
1206
1207 #[test]
1208 fn line_without_duration_has_no_vertex_times() {
1209 let line = line_feature(vec![[0.0, 0.0], [1.0, 1.0]], 1000, None);
1210 let refs = vec![&line];
1211 let layers = build_layers_from_features(&refs, "default").unwrap();
1212 assert!(layers[0].vertex_times.is_none());
1213 }
1214
1215 #[test]
1216 fn matrix_corridor_drops_dead_vertex_times() {
1217 let mut line = line_feature(vec![[0.0, 0.0], [0.0, 1.0], [0.0, 2.0]], 1000, Some(3000));
1222 line.vertex_value_matrix = Some(vec![5.0, 7.0, 5.0, 7.0, 5.0, 7.0]);
1224 let refs = vec![&line];
1225 let layers = build_layers_from_features(&refs, "default").unwrap();
1226 assert!(
1227 layers[0].vertex_times.is_none(),
1228 "matrix corridor must not carry a per-vertex time column"
1229 );
1230 assert!(layers[0].vertex_value_matrix.is_some());
1232 }
1233
1234 fn polygon_feature(corner: [f64; 2], size: f64) -> ParsedFeature {
1236 let [x, y] = corner;
1237 let ring: Vec<Vec<f64>> = vec![
1238 vec![x, y],
1239 vec![x + size, y],
1240 vec![x + size, y + size],
1241 vec![x, y + size],
1242 vec![x, y], ];
1244 ParsedFeature {
1245 geojson: Feature {
1246 bbox: None,
1247 geometry: Some(Geometry::new(GeomValue::Polygon(vec![ring]))),
1248 id: None,
1249 properties: None,
1250 foreign_members: None,
1251 },
1252 shared_properties: None,
1253 timestamp: 1000,
1254 end_timestamp: None,
1255 vertex_timestamps: None,
1256 vertex_values: None,
1257 vertex_value_matrix: None,
1258 lon: x,
1259 lat: y,
1260 }
1261 }
1262
1263 fn polygon_feature_with_hole() -> ParsedFeature {
1266 let exterior: Vec<Vec<f64>> = vec![
1267 vec![0.0, 0.0],
1268 vec![4.0, 0.0],
1269 vec![4.0, 4.0],
1270 vec![0.0, 4.0],
1271 vec![0.0, 0.0],
1272 ];
1273 let hole: Vec<Vec<f64>> = vec![
1274 vec![1.0, 1.0],
1275 vec![2.0, 1.0],
1276 vec![2.0, 2.0],
1277 vec![1.0, 2.0],
1278 vec![1.0, 1.0],
1279 ];
1280 ParsedFeature {
1281 geojson: Feature {
1282 bbox: None,
1283 geometry: Some(Geometry::new(GeomValue::Polygon(vec![exterior, hole]))),
1284 id: None,
1285 properties: None,
1286 foreign_members: None,
1287 },
1288 shared_properties: None,
1289 timestamp: 1000,
1290 end_timestamp: None,
1291 vertex_timestamps: None,
1292 vertex_values: None,
1293 vertex_value_matrix: None,
1294 lon: 2.0,
1295 lat: 2.0,
1296 }
1297 }
1298
1299 #[test]
1300 fn polygon_layer_omits_triangles_by_default() {
1301 let p = polygon_feature([0.0, 0.0], 1.0);
1302 let refs = vec![&p];
1303 let layers = build_layers_from_features(&refs, "default").unwrap();
1304 assert_eq!(layers.len(), 1);
1305 assert!(layers[0].triangles.is_none());
1306 }
1307
1308 #[test]
1309 fn multi_ring_polygon_auto_bakes_triangles_without_flag() {
1310 let holed = polygon_feature_with_hole();
1316 let simple = polygon_feature([10.0, 10.0], 1.0);
1317 let refs = vec![&holed, &simple];
1318 let layers = build_layers_from_features(&refs, "default").unwrap();
1319 assert_eq!(layers.len(), 1);
1320 let tri = layers[0]
1321 .triangles
1322 .as_ref()
1323 .expect("multi-ring layer must auto-bake triangles even without the flag");
1324 assert_eq!(tri.len(), 2);
1325 assert!(!tri[0].is_empty() && tri[0].len() % 3 == 0);
1329 for &i in &tri[0] {
1330 assert!((i as usize) < 10, "triangle index escapes the feature");
1331 }
1332 assert_eq!(tri[1].len(), 6);
1334 }
1335
1336 #[test]
1342 fn synthetic_ids_use_stable_fnv1a() {
1343 assert_eq!(fnv1a_64(b""), 0xcbf2_9ce4_8422_2325);
1345 assert_eq!(fnv1a_64(b"a"), 0xaf63_dc4c_8601_ec8c);
1346 assert_eq!(fnv1a_64(b"foobar"), 0x85944171f73967e8);
1347
1348 let mut f = point_feature(-122.4, 37.7, json!({}));
1350 f.geojson.id = Some(geojson::feature::Id::String("quake-42".to_string()));
1351 assert_eq!(determine_feature_id(&f), fnv1a_64(b"quake-42"));
1352
1353 let f2 = point_feature(-122.4, 37.7, json!({}));
1355 assert_eq!(
1356 determine_feature_id(&f2),
1357 fnv1a_64_fields(&[1000, (-122.4f64).to_bits(), 37.7f64.to_bits()])
1358 );
1359
1360 let seg = crate::clip::ClippedSegment {
1362 tile_x: 0,
1363 tile_y: 0,
1364 zoom: 10,
1365 coordinates: vec![(1.5, 2.5, 0.0)],
1366 timestamps: vec![7],
1367 vertex_values: vec![],
1368 vertex_value_matrix: vec![],
1369 start_time: 7,
1370 end_time: 7,
1371 properties: None,
1372 feature_id: None,
1373 };
1374 assert_eq!(
1375 segment_feature_id(&seg),
1376 fnv1a_64_fields(&[7, 1.5f64.to_bits(), 2.5f64.to_bits()])
1377 );
1378 }
1379
1380 #[test]
1381 fn pre_tessellate_option_bakes_triangle_indices_per_feature() {
1382 let p1 = polygon_feature([0.0, 0.0], 1.0);
1383 let p2 = polygon_feature([5.0, 5.0], 2.0);
1384 let refs = vec![&p1, &p2];
1385 let layers = build_layers_from_features_with(
1386 &refs,
1387 "default",
1388 ColumnarOptions {
1389 pre_tessellate: true,
1390 ..Default::default()
1391 },
1392 )
1393 .unwrap();
1394 assert_eq!(layers.len(), 1);
1395 let tri = layers[0]
1396 .triangles
1397 .as_ref()
1398 .expect("triangles populated when pre_tessellate is on");
1399 assert_eq!(tri.len(), 2);
1400 assert_eq!(tri[0].len(), 6);
1402 assert_eq!(tri[1].len(), 6);
1403 for &i in &tri[0] {
1405 assert!(i < 5);
1406 }
1407 }
1408}