1use std::collections::HashMap;
2use std::io::Cursor;
3use std::ops::Range;
4
5use byteorder::{LittleEndian, ReadBytesExt};
6
7use copc_streaming::VoxelKey;
8
9use crate::error::TemporalError;
10use crate::gps_time::GpsTime;
11use crate::vlr::VlrData;
12
13#[derive(Debug, Clone)]
15pub struct NodeTemporalEntry {
16 pub key: VoxelKey,
18 samples: Vec<GpsTime>,
19}
20
21impl NodeTemporalEntry {
22 pub fn new(key: VoxelKey, samples: Vec<GpsTime>) -> Self {
24 Self { key, samples }
25 }
26
27 pub fn samples(&self) -> &[GpsTime] {
29 &self.samples
30 }
31
32 pub fn time_range(&self) -> (GpsTime, GpsTime) {
34 (self.samples[0], self.samples[self.samples.len() - 1])
35 }
36
37 pub fn overlaps(&self, start: GpsTime, end: GpsTime) -> bool {
39 let (min, max) = self.time_range();
40 max.0 >= start.0 && min.0 <= end.0
42 }
43
44 pub fn estimate_point_range(
52 &self,
53 start: GpsTime,
54 end: GpsTime,
55 stride: u32,
56 point_count: u32,
57 ) -> Range<u32> {
58 if point_count == 0 || self.samples.is_empty() {
59 return 0..0;
60 }
61
62 let i = self.samples.partition_point(|s| s.0 < start.0);
64
65 let past_end = self.samples.partition_point(|s| s.0 <= end.0);
68
69 if i >= past_end {
70 return 0..0;
72 }
73 let j = past_end - 1;
74
75 let start_point = (i as u64 * stride as u64).min(point_count as u64) as u32;
76 let end_point =
77 ((j as u64 * stride as u64 + stride as u64 - 1).min(point_count as u64 - 1)) as u32;
78
79 start_point..(end_point + 1)
80 }
81}
82
83#[derive(Debug, Clone)]
86#[cfg_attr(not(test), allow(dead_code))]
87pub(crate) struct TemporalIndex {
88 version: u32,
89 stride: u32,
90 entries: HashMap<VoxelKey, NodeTemporalEntry>,
91}
92
93#[cfg_attr(not(test), allow(dead_code))]
94impl TemporalIndex {
95 pub fn from_evlrs(evlrs: &[VlrData]) -> Result<Option<Self>, TemporalError> {
100 let vlr = evlrs
101 .iter()
102 .find(|v| v.user_id == "copc_temporal" && v.record_id == 1000);
103
104 let vlr = match vlr {
105 Some(v) => v,
106 None => return Ok(None),
107 };
108
109 let data = &vlr.data;
110 if data.len() < 32 {
111 return Err(TemporalError::TruncatedHeader);
112 }
113
114 let mut cursor = Cursor::new(data);
115
116 let version = cursor.read_u32::<LittleEndian>()?;
117 if version != 1 {
118 return Err(TemporalError::UnsupportedVersion(version));
119 }
120
121 let stride = cursor.read_u32::<LittleEndian>()?;
122 if stride < 1 {
123 return Err(TemporalError::InvalidStride(stride));
124 }
125
126 let node_count = cursor.read_u32::<LittleEndian>()?;
127 let _page_count = cursor.read_u32::<LittleEndian>()?;
128 let _root_page_offset = cursor.read_u64::<LittleEndian>()?;
129 let _root_page_size = cursor.read_u32::<LittleEndian>()?;
130 let _reserved = cursor.read_u32::<LittleEndian>()?;
131
132 let mut entries = HashMap::with_capacity(node_count as usize);
135
136 while entries.len() < node_count as usize {
137 let level = match cursor.read_i32::<LittleEndian>() {
138 Ok(v) => v,
139 Err(e) => return Err(TemporalError::Io(e)),
140 };
141 let x = cursor.read_i32::<LittleEndian>()?;
142 let y = cursor.read_i32::<LittleEndian>()?;
143 let z = cursor.read_i32::<LittleEndian>()?;
144 let sample_count = cursor.read_u32::<LittleEndian>()?;
145
146 if sample_count == 0 {
147 cursor.set_position(cursor.position() + 28);
150 continue;
151 }
152
153 let mut samples = Vec::with_capacity(sample_count as usize);
154 for _ in 0..sample_count {
155 let t = cursor.read_f64::<LittleEndian>()?;
156 samples.push(GpsTime(t));
157 }
158
159 let key = VoxelKey { level, x, y, z };
160
161 entries.insert(key, NodeTemporalEntry { key, samples });
162 }
163
164 Ok(Some(TemporalIndex {
165 version,
166 stride,
167 entries,
168 }))
169 }
170
171 pub fn get(&self, key: &VoxelKey) -> Option<&NodeTemporalEntry> {
173 self.entries.get(key)
174 }
175
176 pub fn nodes_in_range(&self, start: GpsTime, end: GpsTime) -> Vec<&NodeTemporalEntry> {
178 self.entries
179 .values()
180 .filter(|e| e.overlaps(start, end))
181 .collect()
182 }
183
184 pub fn stride(&self) -> u32 {
186 self.stride
187 }
188
189 pub fn version(&self) -> u32 {
191 self.version
192 }
193
194 pub fn len(&self) -> usize {
196 self.entries.len()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use byteorder::WriteBytesExt;
204
205 fn build_evlr_payload(
207 version: u32,
208 stride: u32,
209 nodes: &[(i32, i32, i32, i32, &[f64])],
210 ) -> Vec<u8> {
211 let mut page_size: u32 = 0;
213 for (_, _, _, _, samples) in nodes {
214 page_size += 20 + samples.len() as u32 * 8;
215 }
216
217 let mut buf = Vec::new();
218 buf.write_u32::<LittleEndian>(version).unwrap();
220 buf.write_u32::<LittleEndian>(stride).unwrap();
221 buf.write_u32::<LittleEndian>(nodes.len() as u32).unwrap();
222 buf.write_u32::<LittleEndian>(1).unwrap(); buf.write_u64::<LittleEndian>(32).unwrap(); buf.write_u32::<LittleEndian>(page_size).unwrap();
225 buf.write_u32::<LittleEndian>(0).unwrap(); for &(level, x, y, z, samples) in nodes {
229 buf.write_i32::<LittleEndian>(level).unwrap();
230 buf.write_i32::<LittleEndian>(x).unwrap();
231 buf.write_i32::<LittleEndian>(y).unwrap();
232 buf.write_i32::<LittleEndian>(z).unwrap();
233 buf.write_u32::<LittleEndian>(samples.len() as u32).unwrap();
234 for &s in samples.iter() {
235 buf.write_f64::<LittleEndian>(s).unwrap();
236 }
237 }
238
239 buf
240 }
241
242 fn make_vlr(user_id: &str, record_id: u16, data: Vec<u8>) -> VlrData {
243 VlrData {
244 user_id: user_id.to_string(),
245 record_id,
246 data,
247 }
248 }
249
250 #[test]
251 fn test_parse_roundtrip() {
252 let samples_a: &[f64] = &[100.0, 200.0, 300.0];
253 let samples_b: &[f64] = &[400.0, 500.0];
254 let nodes = vec![(0, 0, 0, 0, samples_a), (1, 1, 0, 0, samples_b)];
255 let data = build_evlr_payload(1, 10, &nodes);
256 let vlr = make_vlr("copc_temporal", 1000, data);
257
258 let index = TemporalIndex::from_evlrs(&[vlr]).unwrap().unwrap();
259 assert_eq!(index.stride(), 10);
260 assert_eq!(index.version(), 1);
261 assert_eq!(index.len(), 2);
262
263 let entry_a = index
264 .get(&VoxelKey {
265 level: 0,
266 x: 0,
267 y: 0,
268 z: 0,
269 })
270 .unwrap();
271 assert_eq!(entry_a.samples().len(), 3);
272 assert_eq!(entry_a.samples()[0], GpsTime(100.0));
273 assert_eq!(entry_a.samples()[2], GpsTime(300.0));
274
275 let entry_b = index
276 .get(&VoxelKey {
277 level: 1,
278 x: 1,
279 y: 0,
280 z: 0,
281 })
282 .unwrap();
283 assert_eq!(entry_b.samples().len(), 2);
284 assert_eq!(entry_b.time_range(), (GpsTime(400.0), GpsTime(500.0)));
285 }
286
287 #[test]
288 fn test_no_temporal_evlr() {
289 let vlr = make_vlr("copc", 1, vec![0; 160]);
290 let result = TemporalIndex::from_evlrs(&[vlr]).unwrap();
291 assert!(result.is_none());
292 }
293
294 #[test]
295 fn test_empty_evlr_list() {
296 let result = TemporalIndex::from_evlrs(&[]).unwrap();
297 assert!(result.is_none());
298 }
299
300 #[test]
301 fn test_wrong_version() {
302 let data = build_evlr_payload(99, 10, &[]);
303 let vlr = make_vlr("copc_temporal", 1000, data);
304 let result = TemporalIndex::from_evlrs(&[vlr]);
305 assert!(matches!(result, Err(TemporalError::UnsupportedVersion(99))));
306 }
307
308 #[test]
309 fn test_truncated_header() {
310 let vlr = make_vlr("copc_temporal", 1000, vec![1, 0, 0, 0]); let result = TemporalIndex::from_evlrs(&[vlr]);
312 assert!(matches!(result, Err(TemporalError::TruncatedHeader)));
313 }
314
315 #[test]
316 fn test_truncated_node_data() {
317 let data = build_evlr_payload(1, 10, &[]);
319 let mut modified = data.clone();
320 modified[8] = 1;
322 let vlr = make_vlr("copc_temporal", 1000, modified);
323 let result = TemporalIndex::from_evlrs(&[vlr]);
324 assert!(result.is_err());
325 }
326
327 #[test]
328 fn test_overlaps_exact_boundaries() {
329 let entry = NodeTemporalEntry {
330 key: VoxelKey {
331 level: 0,
332 x: 0,
333 y: 0,
334 z: 0,
335 },
336 samples: vec![GpsTime(100.0), GpsTime(200.0), GpsTime(300.0)],
337 };
338
339 assert!(entry.overlaps(GpsTime(100.0), GpsTime(300.0)));
341 assert!(entry.overlaps(GpsTime(50.0), GpsTime(100.0)));
343 assert!(entry.overlaps(GpsTime(300.0), GpsTime(400.0)));
345 assert!(!entry.overlaps(GpsTime(0.0), GpsTime(99.9)));
347 assert!(!entry.overlaps(GpsTime(300.1), GpsTime(400.0)));
349 assert!(entry.overlaps(GpsTime(0.0), GpsTime(1000.0)));
351 assert!(entry.overlaps(GpsTime(150.0), GpsTime(250.0)));
353 }
354
355 #[test]
356 fn test_overlaps_single_sample() {
357 let entry = NodeTemporalEntry {
358 key: VoxelKey {
359 level: 0,
360 x: 0,
361 y: 0,
362 z: 0,
363 },
364 samples: vec![GpsTime(100.0)],
365 };
366
367 assert!(entry.overlaps(GpsTime(100.0), GpsTime(100.0)));
368 assert!(entry.overlaps(GpsTime(50.0), GpsTime(150.0)));
369 assert!(!entry.overlaps(GpsTime(50.0), GpsTime(99.9)));
370 assert!(!entry.overlaps(GpsTime(100.1), GpsTime(200.0)));
371 }
372
373 #[test]
374 fn test_nodes_in_range() {
375 let samples_a: &[f64] = &[100.0, 200.0, 300.0];
376 let samples_b: &[f64] = &[400.0, 500.0];
377 let samples_c: &[f64] = &[600.0, 700.0, 800.0];
378 let data = build_evlr_payload(
379 1,
380 10,
381 &[
382 (0, 0, 0, 0, samples_a),
383 (1, 0, 0, 0, samples_b),
384 (1, 1, 0, 0, samples_c),
385 ],
386 );
387 let vlr = make_vlr("copc_temporal", 1000, data);
388 let index = TemporalIndex::from_evlrs(&[vlr]).unwrap().unwrap();
389
390 let result = index.nodes_in_range(GpsTime(250.0), GpsTime(450.0));
392 assert_eq!(result.len(), 2);
393
394 let result = index.nodes_in_range(GpsTime(650.0), GpsTime(750.0));
396 assert_eq!(result.len(), 1);
397 assert_eq!(
398 result[0].key,
399 VoxelKey {
400 level: 1,
401 x: 1,
402 y: 0,
403 z: 0
404 }
405 );
406 }
407
408 #[test]
409 fn test_estimate_point_range_basic() {
410 let entry = NodeTemporalEntry {
412 key: VoxelKey {
413 level: 0,
414 x: 0,
415 y: 0,
416 z: 0,
417 },
418 samples: vec![
419 GpsTime(100.0),
420 GpsTime(200.0),
421 GpsTime(300.0),
422 GpsTime(400.0),
423 GpsTime(450.0),
424 ],
425 };
426
427 let range = entry.estimate_point_range(GpsTime(200.0), GpsTime(400.0), 10, 40);
429 assert_eq!(range, 10..40);
432
433 let range = entry.estimate_point_range(GpsTime(300.0), GpsTime(300.0), 10, 40);
435 assert_eq!(range, 20..30);
438 }
439
440 #[test]
441 fn test_estimate_point_range_no_overlap() {
442 let entry = NodeTemporalEntry {
443 key: VoxelKey {
444 level: 0,
445 x: 0,
446 y: 0,
447 z: 0,
448 },
449 samples: vec![GpsTime(100.0), GpsTime(200.0), GpsTime(300.0)],
450 };
451
452 let range = entry.estimate_point_range(GpsTime(0.0), GpsTime(50.0), 10, 30);
454 assert_eq!(range, 0..0);
455
456 let range = entry.estimate_point_range(GpsTime(400.0), GpsTime(500.0), 10, 30);
458 assert_eq!(range, 0..0);
459 }
460
461 #[test]
462 fn test_estimate_point_range_empty() {
463 let entry = NodeTemporalEntry {
464 key: VoxelKey {
465 level: 0,
466 x: 0,
467 y: 0,
468 z: 0,
469 },
470 samples: vec![],
471 };
472 let range = entry.estimate_point_range(GpsTime(0.0), GpsTime(100.0), 10, 0);
473 assert_eq!(range, 0..0);
474 }
475
476 #[test]
477 fn test_estimate_point_range_stride_1() {
478 let entry = NodeTemporalEntry {
479 key: VoxelKey {
480 level: 0,
481 x: 0,
482 y: 0,
483 z: 0,
484 },
485 samples: vec![
486 GpsTime(1.0),
487 GpsTime(2.0),
488 GpsTime(3.0),
489 GpsTime(4.0),
490 GpsTime(5.0),
491 ],
492 };
493
494 let range = entry.estimate_point_range(GpsTime(2.0), GpsTime(4.0), 1, 5);
496 assert_eq!(range, 1..4);
498 }
499}