1use crate::copc_vlr::CopcInfo;
9use crate::error::CopcError;
10use crate::hierarchy::{HierarchyEntry, query_hierarchy_with_page_pointers};
11use crate::las_header::LasHeader;
12use crate::point::{BoundingBox3D, Point3D};
13use crate::point_format;
14use crate::vlr_chain;
15
16pub struct CopcReader {
29 header: LasHeader,
31 copc_info: CopcInfo,
33 vlrs: Vec<crate::copc_vlr::Vlr>,
35 data: Vec<u8>,
37}
38
39impl CopcReader {
40 pub fn from_bytes(data: &[u8]) -> Result<Self, CopcError> {
49 let header = LasHeader::parse(data)?;
50
51 let vlrs = vlr_chain::parse_vlrs(data, &header)?;
52
53 let copc_info = vlr_chain::find_copc_info(&vlrs)?;
54
55 let _hierarchy_vlr = vlr_chain::find_copc_hierarchy_vlr(&vlrs)?;
57
58 Ok(Self {
59 header,
60 copc_info,
61 vlrs,
62 data: data.to_vec(),
63 })
64 }
65
66 pub fn header(&self) -> &LasHeader {
68 &self.header
69 }
70
71 pub fn copc_info(&self) -> &CopcInfo {
73 &self.copc_info
74 }
75
76 pub fn vlrs(&self) -> &[crate::copc_vlr::Vlr] {
78 &self.vlrs
79 }
80
81 pub fn octree_bounds(&self) -> BoundingBox3D {
83 let (min, max) = self.copc_info.bounds();
84 BoundingBox3D {
85 min_x: min[0],
86 min_y: min[1],
87 min_z: min[2],
88 max_x: max[0],
89 max_y: max[1],
90 max_z: max[2],
91 }
92 }
93
94 pub fn query_points_in_bbox(&self, bbox: &BoundingBox3D) -> Result<Vec<Point3D>, CopcError> {
104 let entries = query_hierarchy_with_page_pointers(
105 &self.data,
106 &self.copc_info,
107 self.copc_info.root_hier_offset,
108 self.copc_info.root_hier_size,
109 bbox,
110 )?;
111
112 let scale = [
113 self.header.scale_x,
114 self.header.scale_y,
115 self.header.scale_z,
116 ];
117 let offset = [
118 self.header.offset_x,
119 self.header.offset_y,
120 self.header.offset_z,
121 ];
122 let format_id = self.header.point_data_format_id;
123 let record_length = self.header.point_data_record_length as usize;
124
125 let mut result: Vec<Point3D> = Vec::new();
126
127 for entry in &entries {
128 let chunk_offset = entry.offset as usize;
129 let chunk_size = entry.byte_count as usize;
130 let point_count = entry.point_count as usize;
131
132 if chunk_offset + chunk_size > self.data.len() {
133 return Err(CopcError::InvalidFormat(format!(
134 "Point data chunk at offset {chunk_offset} + size {chunk_size} \
135 exceeds file length {}",
136 self.data.len()
137 )));
138 }
139
140 let chunk_data = &self.data[chunk_offset..chunk_offset + chunk_size];
141
142 let points = point_format::deserialize_points(
143 chunk_data,
144 point_count,
145 record_length,
146 format_id,
147 scale,
148 offset,
149 )?;
150
151 for pt in points {
153 if bbox.contains(&pt) {
154 result.push(pt);
155 }
156 }
157 }
158
159 Ok(result)
160 }
161
162 pub fn all_points(&self) -> Result<Vec<Point3D>, CopcError> {
169 let full_bbox = self.octree_bounds();
170 let expanded = full_bbox.expand_by(1.0);
172 self.query_points_in_bbox(&expanded)
173 }
174
175 pub fn count_intersecting_chunks(
180 &self,
181 bbox: &BoundingBox3D,
182 ) -> Result<Vec<HierarchyEntry>, CopcError> {
183 query_hierarchy_with_page_pointers(
184 &self.data,
185 &self.copc_info,
186 self.copc_info.root_hier_offset,
187 self.copc_info.root_hier_size,
188 bbox,
189 )
190 }
191
192 pub fn entries_at_depth(&self, depth: i32) -> Result<Vec<HierarchyEntry>, CopcError> {
196 let full_bbox = self.octree_bounds().expand_by(1.0);
197 let all_entries = query_hierarchy_with_page_pointers(
198 &self.data,
199 &self.copc_info,
200 self.copc_info.root_hier_offset,
201 self.copc_info.root_hier_size,
202 &full_bbox,
203 )?;
204 Ok(all_entries
205 .into_iter()
206 .filter(|e| e.key.depth == depth)
207 .collect())
208 }
209}
210
211impl std::fmt::Debug for CopcReader {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.debug_struct("CopcReader")
214 .field("version", &self.header.version)
215 .field("point_format", &self.header.point_data_format_id)
216 .field("record_length", &self.header.point_data_record_length)
217 .field("num_vlrs", &self.vlrs.len())
218 .field("octree_center_x", &self.copc_info.center_x)
219 .field("octree_center_y", &self.copc_info.center_y)
220 .field("octree_center_z", &self.copc_info.center_z)
221 .field("octree_halfsize", &self.copc_info.halfsize)
222 .field("file_size", &self.data.len())
223 .finish()
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 fn build_synthetic_copc(format_id: u8, record_length: u16, points: &[Vec<u8>]) -> Vec<u8> {
240 let header_size: u16 = 227;
244 let mut file = vec![0u8; header_size as usize];
245 file[0..4].copy_from_slice(b"LASF");
246 file[24] = 1; file[25] = 4; file[94..96].copy_from_slice(&header_size.to_le_bytes());
249 file[100..104].copy_from_slice(&2u32.to_le_bytes()); file[104] = format_id;
251 file[105..107].copy_from_slice(&record_length.to_le_bytes());
252 file[107..111].copy_from_slice(&(points.len() as u32).to_le_bytes());
254 let scale = 0.001f64.to_le_bytes();
256 file[131..139].copy_from_slice(&scale);
257 file[139..147].copy_from_slice(&scale);
258 file[147..155].copy_from_slice(&scale);
259 file[179..187].copy_from_slice(&1000.0f64.to_le_bytes()); file[187..195].copy_from_slice(&(-1000.0f64).to_le_bytes()); file[195..203].copy_from_slice(&1000.0f64.to_le_bytes()); file[203..211].copy_from_slice(&(-1000.0f64).to_le_bytes()); file[211..219].copy_from_slice(&1000.0f64.to_le_bytes()); file[219..227].copy_from_slice(&(-1000.0f64).to_le_bytes()); let copc_info_body = {
270 let mut body = vec![0u8; 160];
271 body[0..8].copy_from_slice(&500.0f64.to_le_bytes()); body[8..16].copy_from_slice(&500.0f64.to_le_bytes()); body[16..24].copy_from_slice(&500.0f64.to_le_bytes()); body[24..32].copy_from_slice(&500.0f64.to_le_bytes()); body[32..40].copy_from_slice(&1.0f64.to_le_bytes()); body
278 };
279 append_vlr_to_file(&mut file, "copc", 1, &copc_info_body);
280
281 append_vlr_to_file(&mut file, "copc", 1000, b"");
284
285 let point_data_offset = file.len() as u32;
287 file[96..100].copy_from_slice(&point_data_offset.to_le_bytes());
288
289 let point_data_start = file.len();
291 for p in points {
292 file.extend_from_slice(p);
293 }
294 let point_data_end = file.len();
295 let point_data_size = point_data_end - point_data_start;
296
297 let hier_offset = file.len();
299 let hier_entry = crate::hierarchy::HierarchyEntry {
300 key: crate::hierarchy::VoxelKey {
301 depth: 0,
302 x: 0,
303 y: 0,
304 z: 0,
305 },
306 offset: point_data_start as u64,
307 byte_count: point_data_size as i32,
308 point_count: points.len() as i32,
309 };
310 file.extend_from_slice(&hier_entry.to_bytes());
311 let hier_size = 32u64; let copc_body_offset = header_size as usize + 54;
316 file[copc_body_offset + 40..copc_body_offset + 48]
317 .copy_from_slice(&(hier_offset as u64).to_le_bytes());
318 file[copc_body_offset + 48..copc_body_offset + 56]
319 .copy_from_slice(&hier_size.to_le_bytes());
320
321 file
322 }
323
324 fn append_vlr_to_file(file: &mut Vec<u8>, user_id: &str, record_id: u16, payload: &[u8]) {
325 file.extend_from_slice(&[0u8; 2]);
327 let uid_bytes = user_id.as_bytes();
329 let mut uid_buf = [0u8; 16];
330 let len = uid_bytes.len().min(16);
331 uid_buf[..len].copy_from_slice(&uid_bytes[..len]);
332 file.extend_from_slice(&uid_buf);
333 file.extend_from_slice(&record_id.to_le_bytes());
335 file.extend_from_slice(&(payload.len() as u16).to_le_bytes());
337 file.extend_from_slice(&[0u8; 32]);
339 file.extend_from_slice(payload);
341 }
342
343 fn make_format6_point(raw_x: i32, raw_y: i32, raw_z: i32) -> Vec<u8> {
345 let mut rec = vec![0u8; 30];
346 rec[0..4].copy_from_slice(&raw_x.to_le_bytes());
347 rec[4..8].copy_from_slice(&raw_y.to_le_bytes());
348 rec[8..12].copy_from_slice(&raw_z.to_le_bytes());
349 rec[12..14].copy_from_slice(&100u16.to_le_bytes()); rec[14] = 1 | (1 << 4); rec[16] = 2; rec[22..30].copy_from_slice(&1.0f64.to_le_bytes()); rec
354 }
355
356 fn make_format0_point(raw_x: i32, raw_y: i32, raw_z: i32) -> Vec<u8> {
358 let mut rec = vec![0u8; 20];
359 rec[0..4].copy_from_slice(&raw_x.to_le_bytes());
360 rec[4..8].copy_from_slice(&raw_y.to_le_bytes());
361 rec[8..12].copy_from_slice(&raw_z.to_le_bytes());
362 rec[12..14].copy_from_slice(&50u16.to_le_bytes()); rec[14] = 1 | (1 << 3); rec[15] = 2; rec
366 }
367
368 #[test]
371 fn test_copc_reader_from_bytes_format6() {
372 let points = vec![
373 make_format6_point(100_000, 200_000, 50_000), make_format6_point(300_000, 400_000, 100_000), ];
376 let file_data = build_synthetic_copc(6, 30, &points);
377 let reader = CopcReader::from_bytes(&file_data).expect("should parse synthetic COPC");
378
379 assert_eq!(reader.header().point_data_format_id, 6);
380 assert_eq!(reader.vlrs().len(), 2);
381 assert!((reader.copc_info().center_x - 500.0).abs() < f64::EPSILON);
382 assert!((reader.copc_info().halfsize - 500.0).abs() < f64::EPSILON);
383 }
384
385 #[test]
386 fn test_copc_reader_from_bytes_format0() {
387 let points = vec![
388 make_format0_point(50_000, 50_000, 10_000), ];
390 let file_data = build_synthetic_copc(0, 20, &points);
391 let reader = CopcReader::from_bytes(&file_data).expect("should parse format-0 COPC");
392 assert_eq!(reader.header().point_data_format_id, 0);
393 }
394
395 #[test]
396 fn test_copc_reader_octree_bounds() {
397 let points = vec![make_format6_point(0, 0, 0)];
398 let file_data = build_synthetic_copc(6, 30, &points);
399 let reader = CopcReader::from_bytes(&file_data).expect("parse");
400 let bounds = reader.octree_bounds();
401 assert!((bounds.min_x - 0.0).abs() < 1e-9);
403 assert!((bounds.max_x - 1000.0).abs() < 1e-9);
404 }
405
406 #[test]
409 fn test_query_points_all_inside_bbox() {
410 let points = vec![
411 make_format6_point(100_000, 200_000, 50_000),
412 make_format6_point(300_000, 400_000, 100_000),
413 ];
414 let file_data = build_synthetic_copc(6, 30, &points);
415 let reader = CopcReader::from_bytes(&file_data).expect("parse");
416
417 let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 500.0, 500.0, 500.0).expect("valid bbox");
419 let result = reader.query_points_in_bbox(&bbox).expect("query");
420 assert_eq!(result.len(), 2);
421 }
422
423 #[test]
424 fn test_query_points_partial_bbox() {
425 let points = vec![
426 make_format6_point(100_000, 100_000, 10_000), make_format6_point(800_000, 800_000, 10_000), ];
429 let file_data = build_synthetic_copc(6, 30, &points);
430 let reader = CopcReader::from_bytes(&file_data).expect("parse");
431
432 let bbox = BoundingBox3D::new(50.0, 50.0, 0.0, 200.0, 200.0, 50.0).expect("valid bbox");
434 let result = reader.query_points_in_bbox(&bbox).expect("query");
435 assert_eq!(result.len(), 1);
436 assert!((result[0].x - 100.0).abs() < 1e-6);
437 }
438
439 #[test]
440 fn test_query_points_none_in_bbox() {
441 let points = vec![
442 make_format6_point(100_000, 100_000, 10_000), ];
444 let file_data = build_synthetic_copc(6, 30, &points);
445 let reader = CopcReader::from_bytes(&file_data).expect("parse");
446
447 let bbox =
449 BoundingBox3D::new(500.0, 500.0, 500.0, 600.0, 600.0, 600.0).expect("valid bbox");
450 let result = reader.query_points_in_bbox(&bbox).expect("query");
451 assert!(result.is_empty());
452 }
453
454 #[test]
455 fn test_query_points_coordinates_correct() {
456 let points = vec![make_format6_point(123_456, 789_012, 345_678)];
457 let file_data = build_synthetic_copc(6, 30, &points);
458 let reader = CopcReader::from_bytes(&file_data).expect("parse");
459
460 let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 1000.0, 1000.0, 1000.0).expect("valid bbox");
461 let result = reader.query_points_in_bbox(&bbox).expect("query");
462 assert_eq!(result.len(), 1);
463 assert!((result[0].x - 123.456).abs() < 1e-6);
465 assert!((result[0].y - 789.012).abs() < 1e-6);
466 assert!((result[0].z - 345.678).abs() < 1e-6);
467 }
468
469 #[test]
470 fn test_all_points() {
471 let points = vec![
472 make_format6_point(100_000, 100_000, 100_000),
473 make_format6_point(200_000, 200_000, 200_000),
474 make_format6_point(300_000, 300_000, 300_000),
475 ];
476 let file_data = build_synthetic_copc(6, 30, &points);
477 let reader = CopcReader::from_bytes(&file_data).expect("parse");
478
479 let all = reader.all_points().expect("all_points");
480 assert_eq!(all.len(), 3);
481 }
482
483 #[test]
484 fn test_count_intersecting_chunks() {
485 let points = vec![make_format6_point(500_000, 500_000, 500_000)];
486 let file_data = build_synthetic_copc(6, 30, &points);
487 let reader = CopcReader::from_bytes(&file_data).expect("parse");
488
489 let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 1000.0, 1000.0, 1000.0).expect("valid bbox");
490 let entries = reader.count_intersecting_chunks(&bbox).expect("count");
491 assert_eq!(entries.len(), 1);
492 assert_eq!(entries[0].point_count, 1);
493 }
494
495 #[test]
496 fn test_entries_at_depth() {
497 let points = vec![make_format6_point(500_000, 500_000, 500_000)];
498 let file_data = build_synthetic_copc(6, 30, &points);
499 let reader = CopcReader::from_bytes(&file_data).expect("parse");
500
501 let depth0 = reader.entries_at_depth(0).expect("depth 0");
502 assert_eq!(depth0.len(), 1);
503
504 let depth1 = reader.entries_at_depth(1).expect("depth 1");
505 assert!(depth1.is_empty());
506 }
507
508 #[test]
511 fn test_copc_reader_bad_magic() {
512 let mut file_data = build_synthetic_copc(6, 30, &[]);
513 file_data[0] = b'X'; assert!(CopcReader::from_bytes(&file_data).is_err());
515 }
516
517 #[test]
518 fn test_copc_reader_missing_copc_vlr() {
519 let mut file = vec![0u8; 227];
521 file[0..4].copy_from_slice(b"LASF");
522 file[24] = 1;
523 file[25] = 4;
524 file[94..96].copy_from_slice(&227u16.to_le_bytes());
525 file[96..100].copy_from_slice(&227u32.to_le_bytes());
526 file[100..104].copy_from_slice(&1u32.to_le_bytes()); file[104] = 6;
528 file[105..107].copy_from_slice(&30u16.to_le_bytes());
529 let scale = 0.001f64.to_le_bytes();
530 file[131..139].copy_from_slice(&scale);
531 file[139..147].copy_from_slice(&scale);
532 file[147..155].copy_from_slice(&scale);
533 append_vlr_to_file(&mut file, "laszip", 22204, b"data");
534 assert!(CopcReader::from_bytes(&file).is_err());
535 }
536
537 #[test]
538 fn test_copc_reader_debug_format() {
539 let points = vec![make_format6_point(0, 0, 0)];
540 let file_data = build_synthetic_copc(6, 30, &points);
541 let reader = CopcReader::from_bytes(&file_data).expect("parse");
542 let debug_str = format!("{reader:?}");
543 assert!(debug_str.contains("CopcReader"));
544 assert!(debug_str.contains("point_format"));
545 }
546
547 #[test]
548 fn test_copc_reader_format0_query() {
549 let points = vec![
550 make_format0_point(250_000, 250_000, 50_000), make_format0_point(750_000, 750_000, 50_000), ];
553 let file_data = build_synthetic_copc(0, 20, &points);
554 let reader = CopcReader::from_bytes(&file_data).expect("parse");
555
556 let bbox = BoundingBox3D::new(200.0, 200.0, 0.0, 300.0, 300.0, 100.0).expect("valid bbox");
557 let result = reader.query_points_in_bbox(&bbox).expect("query");
558 assert_eq!(result.len(), 1);
559 assert!((result[0].x - 250.0).abs() < 1e-6);
560 }
561
562 #[test]
563 fn test_copc_reader_multiple_points_ordering() {
564 let points = vec![
565 make_format6_point(100_000, 100_000, 10_000),
566 make_format6_point(200_000, 200_000, 20_000),
567 make_format6_point(300_000, 300_000, 30_000),
568 make_format6_point(400_000, 400_000, 40_000),
569 make_format6_point(500_000, 500_000, 50_000),
570 ];
571 let file_data = build_synthetic_copc(6, 30, &points);
572 let reader = CopcReader::from_bytes(&file_data).expect("parse");
573
574 let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 600.0, 600.0, 100.0).expect("valid bbox");
575 let result = reader.query_points_in_bbox(&bbox).expect("query");
576 assert_eq!(result.len(), 5);
577 assert!((result[0].x - 100.0).abs() < 1e-6);
579 assert!((result[4].x - 500.0).abs() < 1e-6);
580 }
581
582 #[test]
583 fn test_copc_reader_point_attributes_preserved() {
584 let mut rec = make_format6_point(100_000, 200_000, 50_000);
585 rec[12..14].copy_from_slice(&42u16.to_le_bytes()); rec[14] = 2 | (3 << 4); rec[16] = 5; let points = vec![rec];
590 let file_data = build_synthetic_copc(6, 30, &points);
591 let reader = CopcReader::from_bytes(&file_data).expect("parse");
592
593 let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 200.0, 300.0, 100.0).expect("valid bbox");
594 let result = reader.query_points_in_bbox(&bbox).expect("query");
595 assert_eq!(result.len(), 1);
596 assert_eq!(result[0].intensity, 42);
597 assert_eq!(result[0].return_number, 2);
598 assert_eq!(result[0].number_of_returns, 3);
599 assert_eq!(result[0].classification, 5);
600 }
601}