use std::collections::HashMap;
use std::io::Cursor;
use std::ops::Range;
use byteorder::{LittleEndian, ReadBytesExt};
use copc_streaming::VoxelKey;
use crate::error::TemporalError;
use crate::gps_time::GpsTime;
use crate::vlr::VlrData;
#[derive(Debug, Clone)]
pub struct NodeTemporalEntry {
pub key: VoxelKey,
samples: Vec<GpsTime>,
}
impl NodeTemporalEntry {
pub fn new(key: VoxelKey, samples: Vec<GpsTime>) -> Self {
assert!(
!samples.is_empty(),
"NodeTemporalEntry requires at least one sample"
);
Self { key, samples }
}
pub fn samples(&self) -> &[GpsTime] {
&self.samples
}
pub fn time_range(&self) -> (GpsTime, GpsTime) {
(self.samples[0], self.samples[self.samples.len() - 1])
}
pub fn overlaps(&self, start: GpsTime, end: GpsTime) -> bool {
let (min, max) = self.time_range();
max >= start && min <= end
}
pub fn estimate_point_range(
&self,
start: GpsTime,
end: GpsTime,
stride: u32,
point_count: u32,
) -> Range<u32> {
if point_count == 0 {
return 0..0;
}
let i = self.samples.partition_point(|s| *s < start);
let past_end = self.samples.partition_point(|s| *s <= end);
if i >= past_end {
return 0..0;
}
let j = past_end - 1;
let start_point = (i as u64 * stride as u64).min(point_count as u64) as u32;
let end_point =
((j as u64 * stride as u64 + stride as u64 - 1).min(point_count as u64 - 1)) as u32;
start_point..(end_point + 1)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) struct TemporalIndex {
version: u32,
stride: u32,
entries: HashMap<VoxelKey, NodeTemporalEntry>,
}
#[cfg_attr(not(test), allow(dead_code))]
impl TemporalIndex {
pub fn from_evlrs(evlrs: &[VlrData]) -> Result<Option<Self>, TemporalError> {
let vlr = evlrs
.iter()
.find(|v| v.user_id == "copc_temporal" && v.record_id == 1000);
let vlr = match vlr {
Some(v) => v,
None => return Ok(None),
};
let data = &vlr.data;
if data.len() < 32 {
return Err(TemporalError::TruncatedHeader);
}
let mut cursor = Cursor::new(data);
let version = cursor.read_u32::<LittleEndian>()?;
if version != 1 {
return Err(TemporalError::UnsupportedVersion(version));
}
let stride = cursor.read_u32::<LittleEndian>()?;
if stride < 1 {
return Err(TemporalError::InvalidStride(stride));
}
let node_count = cursor.read_u32::<LittleEndian>()?;
let _page_count = cursor.read_u32::<LittleEndian>()?;
let _root_page_offset = cursor.read_u64::<LittleEndian>()?;
let _root_page_size = cursor.read_u32::<LittleEndian>()?;
let _reserved = cursor.read_u32::<LittleEndian>()?;
let mut entries = HashMap::with_capacity(node_count as usize);
while entries.len() < node_count as usize {
let level = match cursor.read_i32::<LittleEndian>() {
Ok(v) => v,
Err(e) => return Err(TemporalError::Io(e)),
};
let x = cursor.read_i32::<LittleEndian>()?;
let y = cursor.read_i32::<LittleEndian>()?;
let z = cursor.read_i32::<LittleEndian>()?;
let sample_count = cursor.read_u32::<LittleEndian>()?;
if sample_count == 0 {
cursor.set_position(cursor.position() + 28);
continue;
}
let mut samples = Vec::with_capacity(sample_count as usize);
for _ in 0..sample_count {
let t = cursor.read_f64::<LittleEndian>()?;
samples.push(GpsTime(t));
}
let key = VoxelKey { level, x, y, z };
entries.insert(key, NodeTemporalEntry { key, samples });
}
Ok(Some(TemporalIndex {
version,
stride,
entries,
}))
}
pub fn get(&self, key: &VoxelKey) -> Option<&NodeTemporalEntry> {
self.entries.get(key)
}
pub fn nodes_in_range(&self, start: GpsTime, end: GpsTime) -> Vec<&NodeTemporalEntry> {
self.entries
.values()
.filter(|e| e.overlaps(start, end))
.collect()
}
pub fn stride(&self) -> u32 {
self.stride
}
pub fn version(&self) -> u32 {
self.version
}
pub fn len(&self) -> usize {
self.entries.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use byteorder::WriteBytesExt;
fn build_evlr_payload(
version: u32,
stride: u32,
nodes: &[(i32, i32, i32, i32, &[f64])],
) -> Vec<u8> {
let mut page_size: u32 = 0;
for (_, _, _, _, samples) in nodes {
page_size += 20 + samples.len() as u32 * 8;
}
let mut buf = Vec::new();
buf.write_u32::<LittleEndian>(version).unwrap();
buf.write_u32::<LittleEndian>(stride).unwrap();
buf.write_u32::<LittleEndian>(nodes.len() as u32).unwrap();
buf.write_u32::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(32).unwrap(); buf.write_u32::<LittleEndian>(page_size).unwrap();
buf.write_u32::<LittleEndian>(0).unwrap();
for &(level, x, y, z, samples) in nodes {
buf.write_i32::<LittleEndian>(level).unwrap();
buf.write_i32::<LittleEndian>(x).unwrap();
buf.write_i32::<LittleEndian>(y).unwrap();
buf.write_i32::<LittleEndian>(z).unwrap();
buf.write_u32::<LittleEndian>(samples.len() as u32).unwrap();
for &s in samples.iter() {
buf.write_f64::<LittleEndian>(s).unwrap();
}
}
buf
}
fn make_vlr(user_id: &str, record_id: u16, data: Vec<u8>) -> VlrData {
VlrData {
user_id: user_id.to_string(),
record_id,
data,
}
}
#[test]
fn test_parse_roundtrip() {
let samples_a: &[f64] = &[100.0, 200.0, 300.0];
let samples_b: &[f64] = &[400.0, 500.0];
let nodes = vec![(0, 0, 0, 0, samples_a), (1, 1, 0, 0, samples_b)];
let data = build_evlr_payload(1, 10, &nodes);
let vlr = make_vlr("copc_temporal", 1000, data);
let index = TemporalIndex::from_evlrs(&[vlr]).unwrap().unwrap();
assert_eq!(index.stride(), 10);
assert_eq!(index.version(), 1);
assert_eq!(index.len(), 2);
let entry_a = index
.get(&VoxelKey {
level: 0,
x: 0,
y: 0,
z: 0,
})
.unwrap();
assert_eq!(entry_a.samples().len(), 3);
assert_eq!(entry_a.samples()[0], GpsTime(100.0));
assert_eq!(entry_a.samples()[2], GpsTime(300.0));
let entry_b = index
.get(&VoxelKey {
level: 1,
x: 1,
y: 0,
z: 0,
})
.unwrap();
assert_eq!(entry_b.samples().len(), 2);
assert_eq!(entry_b.time_range(), (GpsTime(400.0), GpsTime(500.0)));
}
#[test]
fn test_no_temporal_evlr() {
let vlr = make_vlr("copc", 1, vec![0; 160]);
let result = TemporalIndex::from_evlrs(&[vlr]).unwrap();
assert!(result.is_none());
}
#[test]
fn test_empty_evlr_list() {
let result = TemporalIndex::from_evlrs(&[]).unwrap();
assert!(result.is_none());
}
#[test]
fn test_wrong_version() {
let data = build_evlr_payload(99, 10, &[]);
let vlr = make_vlr("copc_temporal", 1000, data);
let result = TemporalIndex::from_evlrs(&[vlr]);
assert!(matches!(result, Err(TemporalError::UnsupportedVersion(99))));
}
#[test]
fn test_truncated_header() {
let vlr = make_vlr("copc_temporal", 1000, vec![1, 0, 0, 0]); let result = TemporalIndex::from_evlrs(&[vlr]);
assert!(matches!(result, Err(TemporalError::TruncatedHeader)));
}
#[test]
fn test_truncated_node_data() {
let data = build_evlr_payload(1, 10, &[]);
let mut modified = data.clone();
modified[8] = 1;
let vlr = make_vlr("copc_temporal", 1000, modified);
let result = TemporalIndex::from_evlrs(&[vlr]);
assert!(result.is_err());
}
#[test]
fn test_overlaps_exact_boundaries() {
let entry = NodeTemporalEntry {
key: VoxelKey {
level: 0,
x: 0,
y: 0,
z: 0,
},
samples: vec![GpsTime(100.0), GpsTime(200.0), GpsTime(300.0)],
};
assert!(entry.overlaps(GpsTime(100.0), GpsTime(300.0)));
assert!(entry.overlaps(GpsTime(50.0), GpsTime(100.0)));
assert!(entry.overlaps(GpsTime(300.0), GpsTime(400.0)));
assert!(!entry.overlaps(GpsTime(0.0), GpsTime(99.9)));
assert!(!entry.overlaps(GpsTime(300.1), GpsTime(400.0)));
assert!(entry.overlaps(GpsTime(0.0), GpsTime(1000.0)));
assert!(entry.overlaps(GpsTime(150.0), GpsTime(250.0)));
}
#[test]
fn test_overlaps_single_sample() {
let entry = NodeTemporalEntry {
key: VoxelKey {
level: 0,
x: 0,
y: 0,
z: 0,
},
samples: vec![GpsTime(100.0)],
};
assert!(entry.overlaps(GpsTime(100.0), GpsTime(100.0)));
assert!(entry.overlaps(GpsTime(50.0), GpsTime(150.0)));
assert!(!entry.overlaps(GpsTime(50.0), GpsTime(99.9)));
assert!(!entry.overlaps(GpsTime(100.1), GpsTime(200.0)));
}
#[test]
fn test_nodes_in_range() {
let samples_a: &[f64] = &[100.0, 200.0, 300.0];
let samples_b: &[f64] = &[400.0, 500.0];
let samples_c: &[f64] = &[600.0, 700.0, 800.0];
let data = build_evlr_payload(
1,
10,
&[
(0, 0, 0, 0, samples_a),
(1, 0, 0, 0, samples_b),
(1, 1, 0, 0, samples_c),
],
);
let vlr = make_vlr("copc_temporal", 1000, data);
let index = TemporalIndex::from_evlrs(&[vlr]).unwrap().unwrap();
let result = index.nodes_in_range(GpsTime(250.0), GpsTime(450.0));
assert_eq!(result.len(), 2);
let result = index.nodes_in_range(GpsTime(650.0), GpsTime(750.0));
assert_eq!(result.len(), 1);
assert_eq!(
result[0].key,
VoxelKey {
level: 1,
x: 1,
y: 0,
z: 0
}
);
}
#[test]
fn test_estimate_point_range_basic() {
let entry = NodeTemporalEntry {
key: VoxelKey {
level: 0,
x: 0,
y: 0,
z: 0,
},
samples: vec![
GpsTime(100.0),
GpsTime(200.0),
GpsTime(300.0),
GpsTime(400.0),
GpsTime(450.0),
],
};
let range = entry.estimate_point_range(GpsTime(200.0), GpsTime(400.0), 10, 40);
assert_eq!(range, 10..40);
let range = entry.estimate_point_range(GpsTime(300.0), GpsTime(300.0), 10, 40);
assert_eq!(range, 20..30);
}
#[test]
fn test_estimate_point_range_no_overlap() {
let entry = NodeTemporalEntry {
key: VoxelKey {
level: 0,
x: 0,
y: 0,
z: 0,
},
samples: vec![GpsTime(100.0), GpsTime(200.0), GpsTime(300.0)],
};
let range = entry.estimate_point_range(GpsTime(0.0), GpsTime(50.0), 10, 30);
assert_eq!(range, 0..0);
let range = entry.estimate_point_range(GpsTime(400.0), GpsTime(500.0), 10, 30);
assert_eq!(range, 0..0);
}
#[test]
fn test_estimate_point_range_stride_1() {
let entry = NodeTemporalEntry {
key: VoxelKey {
level: 0,
x: 0,
y: 0,
z: 0,
},
samples: vec![
GpsTime(1.0),
GpsTime(2.0),
GpsTime(3.0),
GpsTime(4.0),
GpsTime(5.0),
],
};
let range = entry.estimate_point_range(GpsTime(2.0), GpsTime(4.0), 1, 5);
assert_eq!(range, 1..4);
}
}