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