use crate::copc_vlr::CopcInfo;
use crate::error::CopcError;
use crate::hierarchy::{HierarchyEntry, query_hierarchy_with_page_pointers};
use crate::las_header::LasHeader;
use crate::point::{BoundingBox3D, Point3D};
use crate::point_format;
use crate::vlr_chain;
pub struct CopcReader {
header: LasHeader,
copc_info: CopcInfo,
vlrs: Vec<crate::copc_vlr::Vlr>,
data: Vec<u8>,
}
impl CopcReader {
pub fn from_bytes(data: &[u8]) -> Result<Self, CopcError> {
let header = LasHeader::parse(data)?;
let vlrs = vlr_chain::parse_vlrs(data, &header)?;
let copc_info = vlr_chain::find_copc_info(&vlrs)?;
let _hierarchy_vlr = vlr_chain::find_copc_hierarchy_vlr(&vlrs)?;
Ok(Self {
header,
copc_info,
vlrs,
data: data.to_vec(),
})
}
pub fn header(&self) -> &LasHeader {
&self.header
}
pub fn copc_info(&self) -> &CopcInfo {
&self.copc_info
}
pub fn vlrs(&self) -> &[crate::copc_vlr::Vlr] {
&self.vlrs
}
pub fn octree_bounds(&self) -> BoundingBox3D {
let (min, max) = self.copc_info.bounds();
BoundingBox3D {
min_x: min[0],
min_y: min[1],
min_z: min[2],
max_x: max[0],
max_y: max[1],
max_z: max[2],
}
}
pub fn query_points_in_bbox(&self, bbox: &BoundingBox3D) -> Result<Vec<Point3D>, CopcError> {
let entries = query_hierarchy_with_page_pointers(
&self.data,
&self.copc_info,
self.copc_info.root_hier_offset,
self.copc_info.root_hier_size,
bbox,
)?;
let scale = [
self.header.scale_x,
self.header.scale_y,
self.header.scale_z,
];
let offset = [
self.header.offset_x,
self.header.offset_y,
self.header.offset_z,
];
let format_id = self.header.point_data_format_id;
let record_length = self.header.point_data_record_length as usize;
let mut result: Vec<Point3D> = Vec::new();
for entry in &entries {
let chunk_offset = entry.offset as usize;
let chunk_size = entry.byte_count as usize;
let point_count = entry.point_count as usize;
if chunk_offset + chunk_size > self.data.len() {
return Err(CopcError::InvalidFormat(format!(
"Point data chunk at offset {chunk_offset} + size {chunk_size} \
exceeds file length {}",
self.data.len()
)));
}
let chunk_data = &self.data[chunk_offset..chunk_offset + chunk_size];
let points = point_format::deserialize_points(
chunk_data,
point_count,
record_length,
format_id,
scale,
offset,
)?;
for pt in points {
if bbox.contains(&pt) {
result.push(pt);
}
}
}
Ok(result)
}
pub fn all_points(&self) -> Result<Vec<Point3D>, CopcError> {
let full_bbox = self.octree_bounds();
let expanded = full_bbox.expand_by(1.0);
self.query_points_in_bbox(&expanded)
}
pub fn count_intersecting_chunks(
&self,
bbox: &BoundingBox3D,
) -> Result<Vec<HierarchyEntry>, CopcError> {
query_hierarchy_with_page_pointers(
&self.data,
&self.copc_info,
self.copc_info.root_hier_offset,
self.copc_info.root_hier_size,
bbox,
)
}
pub fn entries_at_depth(&self, depth: i32) -> Result<Vec<HierarchyEntry>, CopcError> {
let full_bbox = self.octree_bounds().expand_by(1.0);
let all_entries = query_hierarchy_with_page_pointers(
&self.data,
&self.copc_info,
self.copc_info.root_hier_offset,
self.copc_info.root_hier_size,
&full_bbox,
)?;
Ok(all_entries
.into_iter()
.filter(|e| e.key.depth == depth)
.collect())
}
}
impl std::fmt::Debug for CopcReader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CopcReader")
.field("version", &self.header.version)
.field("point_format", &self.header.point_data_format_id)
.field("record_length", &self.header.point_data_record_length)
.field("num_vlrs", &self.vlrs.len())
.field("octree_center_x", &self.copc_info.center_x)
.field("octree_center_y", &self.copc_info.center_y)
.field("octree_center_z", &self.copc_info.center_z)
.field("octree_halfsize", &self.copc_info.halfsize)
.field("file_size", &self.data.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_synthetic_copc(format_id: u8, record_length: u16, points: &[Vec<u8>]) -> Vec<u8> {
let header_size: u16 = 227;
let mut file = vec![0u8; header_size as usize];
file[0..4].copy_from_slice(b"LASF");
file[24] = 1; file[25] = 4; file[94..96].copy_from_slice(&header_size.to_le_bytes());
file[100..104].copy_from_slice(&2u32.to_le_bytes()); file[104] = format_id;
file[105..107].copy_from_slice(&record_length.to_le_bytes());
file[107..111].copy_from_slice(&(points.len() as u32).to_le_bytes());
let scale = 0.001f64.to_le_bytes();
file[131..139].copy_from_slice(&scale);
file[139..147].copy_from_slice(&scale);
file[147..155].copy_from_slice(&scale);
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 = {
let mut body = vec![0u8; 160];
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
};
append_vlr_to_file(&mut file, "copc", 1, &copc_info_body);
append_vlr_to_file(&mut file, "copc", 1000, b"");
let point_data_offset = file.len() as u32;
file[96..100].copy_from_slice(&point_data_offset.to_le_bytes());
let point_data_start = file.len();
for p in points {
file.extend_from_slice(p);
}
let point_data_end = file.len();
let point_data_size = point_data_end - point_data_start;
let hier_offset = file.len();
let hier_entry = crate::hierarchy::HierarchyEntry {
key: crate::hierarchy::VoxelKey {
depth: 0,
x: 0,
y: 0,
z: 0,
},
offset: point_data_start as u64,
byte_count: point_data_size as i32,
point_count: points.len() as i32,
};
file.extend_from_slice(&hier_entry.to_bytes());
let hier_size = 32u64;
let copc_body_offset = header_size as usize + 54;
file[copc_body_offset + 40..copc_body_offset + 48]
.copy_from_slice(&(hier_offset as u64).to_le_bytes());
file[copc_body_offset + 48..copc_body_offset + 56]
.copy_from_slice(&hier_size.to_le_bytes());
file
}
fn append_vlr_to_file(file: &mut Vec<u8>, user_id: &str, record_id: u16, payload: &[u8]) {
file.extend_from_slice(&[0u8; 2]);
let uid_bytes = user_id.as_bytes();
let mut uid_buf = [0u8; 16];
let len = uid_bytes.len().min(16);
uid_buf[..len].copy_from_slice(&uid_bytes[..len]);
file.extend_from_slice(&uid_buf);
file.extend_from_slice(&record_id.to_le_bytes());
file.extend_from_slice(&(payload.len() as u16).to_le_bytes());
file.extend_from_slice(&[0u8; 32]);
file.extend_from_slice(payload);
}
fn make_format6_point(raw_x: i32, raw_y: i32, raw_z: i32) -> Vec<u8> {
let mut rec = vec![0u8; 30];
rec[0..4].copy_from_slice(&raw_x.to_le_bytes());
rec[4..8].copy_from_slice(&raw_y.to_le_bytes());
rec[8..12].copy_from_slice(&raw_z.to_le_bytes());
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
}
fn make_format0_point(raw_x: i32, raw_y: i32, raw_z: i32) -> Vec<u8> {
let mut rec = vec![0u8; 20];
rec[0..4].copy_from_slice(&raw_x.to_le_bytes());
rec[4..8].copy_from_slice(&raw_y.to_le_bytes());
rec[8..12].copy_from_slice(&raw_z.to_le_bytes());
rec[12..14].copy_from_slice(&50u16.to_le_bytes()); rec[14] = 1 | (1 << 3); rec[15] = 2; rec
}
#[test]
fn test_copc_reader_from_bytes_format6() {
let points = vec![
make_format6_point(100_000, 200_000, 50_000), make_format6_point(300_000, 400_000, 100_000), ];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("should parse synthetic COPC");
assert_eq!(reader.header().point_data_format_id, 6);
assert_eq!(reader.vlrs().len(), 2);
assert!((reader.copc_info().center_x - 500.0).abs() < f64::EPSILON);
assert!((reader.copc_info().halfsize - 500.0).abs() < f64::EPSILON);
}
#[test]
fn test_copc_reader_from_bytes_format0() {
let points = vec![
make_format0_point(50_000, 50_000, 10_000), ];
let file_data = build_synthetic_copc(0, 20, &points);
let reader = CopcReader::from_bytes(&file_data).expect("should parse format-0 COPC");
assert_eq!(reader.header().point_data_format_id, 0);
}
#[test]
fn test_copc_reader_octree_bounds() {
let points = vec![make_format6_point(0, 0, 0)];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bounds = reader.octree_bounds();
assert!((bounds.min_x - 0.0).abs() < 1e-9);
assert!((bounds.max_x - 1000.0).abs() < 1e-9);
}
#[test]
fn test_query_points_all_inside_bbox() {
let points = vec![
make_format6_point(100_000, 200_000, 50_000),
make_format6_point(300_000, 400_000, 100_000),
];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 500.0, 500.0, 500.0).expect("valid bbox");
let result = reader.query_points_in_bbox(&bbox).expect("query");
assert_eq!(result.len(), 2);
}
#[test]
fn test_query_points_partial_bbox() {
let points = vec![
make_format6_point(100_000, 100_000, 10_000), make_format6_point(800_000, 800_000, 10_000), ];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bbox = BoundingBox3D::new(50.0, 50.0, 0.0, 200.0, 200.0, 50.0).expect("valid bbox");
let result = reader.query_points_in_bbox(&bbox).expect("query");
assert_eq!(result.len(), 1);
assert!((result[0].x - 100.0).abs() < 1e-6);
}
#[test]
fn test_query_points_none_in_bbox() {
let points = vec![
make_format6_point(100_000, 100_000, 10_000), ];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bbox =
BoundingBox3D::new(500.0, 500.0, 500.0, 600.0, 600.0, 600.0).expect("valid bbox");
let result = reader.query_points_in_bbox(&bbox).expect("query");
assert!(result.is_empty());
}
#[test]
fn test_query_points_coordinates_correct() {
let points = vec![make_format6_point(123_456, 789_012, 345_678)];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 1000.0, 1000.0, 1000.0).expect("valid bbox");
let result = reader.query_points_in_bbox(&bbox).expect("query");
assert_eq!(result.len(), 1);
assert!((result[0].x - 123.456).abs() < 1e-6);
assert!((result[0].y - 789.012).abs() < 1e-6);
assert!((result[0].z - 345.678).abs() < 1e-6);
}
#[test]
fn test_all_points() {
let points = vec![
make_format6_point(100_000, 100_000, 100_000),
make_format6_point(200_000, 200_000, 200_000),
make_format6_point(300_000, 300_000, 300_000),
];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let all = reader.all_points().expect("all_points");
assert_eq!(all.len(), 3);
}
#[test]
fn test_count_intersecting_chunks() {
let points = vec![make_format6_point(500_000, 500_000, 500_000)];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 1000.0, 1000.0, 1000.0).expect("valid bbox");
let entries = reader.count_intersecting_chunks(&bbox).expect("count");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].point_count, 1);
}
#[test]
fn test_entries_at_depth() {
let points = vec![make_format6_point(500_000, 500_000, 500_000)];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let depth0 = reader.entries_at_depth(0).expect("depth 0");
assert_eq!(depth0.len(), 1);
let depth1 = reader.entries_at_depth(1).expect("depth 1");
assert!(depth1.is_empty());
}
#[test]
fn test_copc_reader_bad_magic() {
let mut file_data = build_synthetic_copc(6, 30, &[]);
file_data[0] = b'X'; assert!(CopcReader::from_bytes(&file_data).is_err());
}
#[test]
fn test_copc_reader_missing_copc_vlr() {
let mut file = vec![0u8; 227];
file[0..4].copy_from_slice(b"LASF");
file[24] = 1;
file[25] = 4;
file[94..96].copy_from_slice(&227u16.to_le_bytes());
file[96..100].copy_from_slice(&227u32.to_le_bytes());
file[100..104].copy_from_slice(&1u32.to_le_bytes()); file[104] = 6;
file[105..107].copy_from_slice(&30u16.to_le_bytes());
let scale = 0.001f64.to_le_bytes();
file[131..139].copy_from_slice(&scale);
file[139..147].copy_from_slice(&scale);
file[147..155].copy_from_slice(&scale);
append_vlr_to_file(&mut file, "laszip", 22204, b"data");
assert!(CopcReader::from_bytes(&file).is_err());
}
#[test]
fn test_copc_reader_debug_format() {
let points = vec![make_format6_point(0, 0, 0)];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let debug_str = format!("{reader:?}");
assert!(debug_str.contains("CopcReader"));
assert!(debug_str.contains("point_format"));
}
#[test]
fn test_copc_reader_format0_query() {
let points = vec![
make_format0_point(250_000, 250_000, 50_000), make_format0_point(750_000, 750_000, 50_000), ];
let file_data = build_synthetic_copc(0, 20, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bbox = BoundingBox3D::new(200.0, 200.0, 0.0, 300.0, 300.0, 100.0).expect("valid bbox");
let result = reader.query_points_in_bbox(&bbox).expect("query");
assert_eq!(result.len(), 1);
assert!((result[0].x - 250.0).abs() < 1e-6);
}
#[test]
fn test_copc_reader_multiple_points_ordering() {
let points = vec![
make_format6_point(100_000, 100_000, 10_000),
make_format6_point(200_000, 200_000, 20_000),
make_format6_point(300_000, 300_000, 30_000),
make_format6_point(400_000, 400_000, 40_000),
make_format6_point(500_000, 500_000, 50_000),
];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 600.0, 600.0, 100.0).expect("valid bbox");
let result = reader.query_points_in_bbox(&bbox).expect("query");
assert_eq!(result.len(), 5);
assert!((result[0].x - 100.0).abs() < 1e-6);
assert!((result[4].x - 500.0).abs() < 1e-6);
}
#[test]
fn test_copc_reader_point_attributes_preserved() {
let mut rec = make_format6_point(100_000, 200_000, 50_000);
rec[12..14].copy_from_slice(&42u16.to_le_bytes()); rec[14] = 2 | (3 << 4); rec[16] = 5;
let points = vec![rec];
let file_data = build_synthetic_copc(6, 30, &points);
let reader = CopcReader::from_bytes(&file_data).expect("parse");
let bbox = BoundingBox3D::new(0.0, 0.0, 0.0, 200.0, 300.0, 100.0).expect("valid bbox");
let result = reader.query_points_in_bbox(&bbox).expect("query");
assert_eq!(result.len(), 1);
assert_eq!(result[0].intensity, 42);
assert_eq!(result[0].return_number, 2);
assert_eq!(result[0].number_of_returns, 3);
assert_eq!(result[0].classification, 5);
}
}