use std::io::Cursor;
use las::{PointData, PointDataBuilder};
use laz::LazVlr;
use laz::record::{LayeredPointRecordDecompressor, RecordDecompressor};
use crate::byte_source::ByteSource;
use crate::error::CopcError;
use crate::fields::Fields;
use crate::hierarchy::HierarchyEntry;
use crate::types::{Aabb, VoxelKey};
#[non_exhaustive]
pub struct Chunk {
pub key: VoxelKey,
pub fields: Fields,
cloud: PointData,
}
impl Chunk {
pub fn new(key: VoxelKey, fields: Fields, cloud: PointData) -> Self {
Self { key, fields, cloud }
}
pub fn cloud(&self) -> &PointData {
&self.cloud
}
pub fn point_count(&self) -> usize {
self.cloud.len()
}
pub fn is_empty(&self) -> bool {
self.cloud.is_empty()
}
pub fn to_points(&self) -> Result<Vec<las::Point>, CopcError> {
if self.fields != Fields::ALL {
return Err(CopcError::PartialDecode(self.fields));
}
self.cloud
.points()
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(CopcError::Las)
}
pub fn points_at(&self, indices: &[u32]) -> Result<Vec<las::Point>, CopcError> {
if self.fields != Fields::ALL {
return Err(CopcError::PartialDecode(self.fields));
}
let record_len = self.cloud.record_len();
let format = self.cloud.format();
let transforms = self.cloud.transforms();
let bytes = self.cloud.raw_bytes();
indices
.iter()
.map(|&i| {
let start = i as usize * record_len;
let end = start + record_len;
let mut cursor = Cursor::new(&bytes[start..end]);
let raw = las::raw::Point::read_from(&mut cursor, format)?;
Ok(las::Point::new(raw, transforms))
})
.collect()
}
pub fn positions(&self) -> Option<impl Iterator<Item = [f64; 3]> + '_> {
if !self.fields.contains(Fields::Z) {
return None;
}
Some(
self.cloud
.x()
.zip(self.cloud.y())
.zip(self.cloud.z())
.map(|((x, y), z)| [x, y, z]),
)
}
pub fn intensity(&self) -> Option<impl Iterator<Item = u16> + '_> {
if !self.fields.contains(Fields::INTENSITY) {
return None;
}
Some(self.cloud.intensity())
}
pub fn classification(&self) -> Option<impl Iterator<Item = u8> + '_> {
if !self.fields.contains(Fields::CLASSIFICATION) {
return None;
}
Some(self.cloud.classification())
}
pub fn scan_angle(&self) -> Option<impl Iterator<Item = f32> + '_> {
if !self.fields.contains(Fields::SCAN_ANGLE) {
return None;
}
Some(self.cloud.scan_angle_degrees())
}
pub fn user_data(&self) -> Option<impl Iterator<Item = u8> + '_> {
if !self.fields.contains(Fields::USER_DATA) {
return None;
}
Some(self.cloud.user_data())
}
pub fn point_source_id(&self) -> Option<impl Iterator<Item = u16> + '_> {
if !self.fields.contains(Fields::POINT_SOURCE_ID) {
return None;
}
Some(self.cloud.point_source_id())
}
pub fn gps_time(&self) -> Option<impl Iterator<Item = f64> + '_> {
if !self.fields.contains(Fields::GPS_TIME) {
return None;
}
self.cloud.gps_time()
}
pub fn rgb(&self) -> Option<impl Iterator<Item = (u16, u16, u16)> + '_> {
if !self.fields.contains(Fields::RGB) {
return None;
}
self.cloud.rgb()
}
pub fn nir(&self) -> Option<impl Iterator<Item = u16> + '_> {
if !self.fields.contains(Fields::NIR) {
return None;
}
self.cloud.nir()
}
pub fn indices_in_bounds(&self, bounds: &Aabb) -> Option<Vec<u32>> {
let positions = self.positions()?;
Some(
positions
.enumerate()
.filter_map(|(i, [x, y, z])| {
let inside = x >= bounds.min[0]
&& x <= bounds.max[0]
&& y >= bounds.min[1]
&& y <= bounds.max[1]
&& z >= bounds.min[2]
&& z <= bounds.max[2];
inside.then_some(i as u32)
})
.collect(),
)
}
pub fn decompress(
compressed: &[u8],
entry: &HierarchyEntry,
laz_vlr: &LazVlr,
header: &las::Header,
fields: Fields,
) -> Result<Self, CopcError> {
decompress_chunk(compressed, entry, laz_vlr, header, fields)
}
}
pub(crate) fn decompress_chunk(
compressed: &[u8],
entry: &HierarchyEntry,
laz_vlr: &LazVlr,
header: &las::Header,
fields: Fields,
) -> Result<Chunk, CopcError> {
let format = *header.point_format();
let transforms = *header.transforms();
let record_len = format.len() as usize;
let decompressed_size = entry.point_count as usize * record_len;
let mut decompressed = vec![0u8; decompressed_size];
decompress_copc_chunk(compressed, &mut decompressed, laz_vlr, fields)?;
let cloud = PointDataBuilder::new()
.with_format(format)
.with_transforms(transforms)
.build_from_bytes(decompressed)?;
Ok(Chunk {
key: entry.key,
fields,
cloud,
})
}
pub(crate) async fn fetch_and_decompress(
source: &impl ByteSource,
entry: &HierarchyEntry,
laz_vlr: &LazVlr,
header: &las::Header,
fields: Fields,
) -> Result<Chunk, CopcError> {
let compressed = source
.read_range(entry.offset, entry.byte_size as u64)
.await?;
decompress_chunk(&compressed, entry, laz_vlr, header, fields)
}
fn decompress_copc_chunk(
compressed: &[u8],
decompressed: &mut [u8],
laz_vlr: &LazVlr,
fields: Fields,
) -> Result<(), CopcError> {
let src = Cursor::new(compressed);
let mut decompressor = LayeredPointRecordDecompressor::new(src);
decompressor.set_fields_from(laz_vlr.items())?;
decompressor.set_selection(fields.to_laz_selection());
decompressor.decompress_many(decompressed)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use las::point::Format;
use las::raw::point::{Flags, ScanAngle};
fn build_test_cloud(n: i32) -> las::PointData {
let format = Format::new(7).unwrap();
let unit = las::Transform {
scale: 1.0,
offset: 0.0,
};
let transforms = las::Vector {
x: unit,
y: unit,
z: unit,
};
let mut buf = Vec::new();
for i in 0..n {
let rp = las::raw::Point {
x: i,
y: i + 1,
z: i + 2,
intensity: 100 + i as u16,
flags: Flags::ThreeByte(0, 0, 2),
scan_angle: ScanAngle::Scaled(0),
user_data: 0,
point_source_id: 0,
gps_time: Some(1000.0 + f64::from(i)),
color: Some(las::Color {
red: i as u16,
green: i as u16,
blue: i as u16,
}),
waveform: None,
nir: None,
extra_bytes: Vec::new(),
};
rp.write_to(&mut buf, &format).unwrap();
}
PointDataBuilder::new()
.with_format(format)
.with_transforms(transforms)
.build_from_bytes(buf)
.unwrap()
}
fn make_chunk(cloud: las::PointData, fields: Fields) -> Chunk {
Chunk {
key: VoxelKey::ROOT,
fields,
cloud,
}
}
#[test]
fn point_count_and_empty() {
let cloud = build_test_cloud(5);
let chunk = make_chunk(cloud, Fields::ALL);
assert_eq!(chunk.point_count(), 5);
assert!(!chunk.is_empty());
}
#[test]
fn positions_iterate_correctly() {
let chunk = make_chunk(build_test_cloud(3), Fields::ALL);
let positions: Vec<_> = chunk.positions().unwrap().collect();
assert_eq!(positions.len(), 3);
assert_eq!(positions[0], [0.0, 1.0, 2.0]);
assert_eq!(positions[2], [2.0, 3.0, 4.0]);
}
#[test]
fn positions_returns_none_without_z_field() {
let chunk = make_chunk(build_test_cloud(3), Fields::empty());
assert!(chunk.positions().is_none());
}
#[test]
fn gps_time_column_guarded_by_fields() {
let chunk = make_chunk(build_test_cloud(3), Fields::Z);
assert!(
chunk.gps_time().is_none(),
"GPS_TIME not in mask -> column should be None"
);
}
#[test]
fn gps_time_column_present_when_fields_allow() {
let chunk = make_chunk(build_test_cloud(3), Fields::ALL);
let times: Vec<_> = chunk.gps_time().unwrap().collect();
assert_eq!(times, vec![1000.0, 1001.0, 1002.0]);
}
#[test]
fn rgb_column_guarded_by_fields() {
let chunk = make_chunk(build_test_cloud(3), Fields::Z | Fields::GPS_TIME);
assert!(chunk.rgb().is_none());
}
#[test]
fn rgb_column_present_when_fields_allow() {
let chunk = make_chunk(build_test_cloud(3), Fields::ALL);
let rgb: Vec<_> = chunk.rgb().unwrap().collect();
assert_eq!(rgb, vec![(0, 0, 0), (1, 1, 1), (2, 2, 2)]);
}
#[test]
fn intensity_column_guarded() {
let chunk = make_chunk(build_test_cloud(3), Fields::Z);
assert!(chunk.intensity().is_none());
let chunk = make_chunk(build_test_cloud(3), Fields::Z | Fields::INTENSITY);
let intensities: Vec<_> = chunk.intensity().unwrap().collect();
assert_eq!(intensities, vec![100, 101, 102]);
}
#[test]
fn to_points_refuses_partial_fields() {
let chunk = make_chunk(build_test_cloud(3), Fields::Z);
let r = chunk.to_points();
assert!(matches!(r, Err(CopcError::PartialDecode(_))));
}
#[test]
fn to_points_succeeds_with_all_fields() {
let chunk = make_chunk(build_test_cloud(3), Fields::ALL);
let pts = chunk.to_points().unwrap();
assert_eq!(pts.len(), 3);
assert_eq!(pts[0].intensity, 100);
assert_eq!(pts[1].gps_time, Some(1001.0));
}
#[test]
fn points_at_materializes_subset() {
let chunk = make_chunk(build_test_cloud(5), Fields::ALL);
let pts = chunk.points_at(&[0, 2, 4]).unwrap();
assert_eq!(pts.len(), 3);
assert_eq!(pts[0].x, 0.0);
assert_eq!(pts[1].x, 2.0);
assert_eq!(pts[2].x, 4.0);
assert_eq!(pts[0].intensity, 100);
assert_eq!(pts[2].intensity, 104);
}
#[test]
fn points_at_refuses_partial_fields() {
let chunk = make_chunk(build_test_cloud(3), Fields::Z);
let r = chunk.points_at(&[0, 1]);
assert!(matches!(r, Err(CopcError::PartialDecode(_))));
}
#[test]
fn points_at_empty_slice_returns_empty_vec() {
let chunk = make_chunk(build_test_cloud(3), Fields::ALL);
let pts = chunk.points_at(&[]).unwrap();
assert!(pts.is_empty());
}
#[test]
fn indices_in_bounds_filters_correctly() {
let chunk = make_chunk(build_test_cloud(5), Fields::ALL);
let bounds = Aabb {
min: [1.0, 0.0, 0.0],
max: [2.0, 10.0, 10.0],
};
assert_eq!(chunk.indices_in_bounds(&bounds).unwrap(), vec![1, 2]);
}
#[test]
fn indices_in_bounds_empty_when_outside() {
let chunk = make_chunk(build_test_cloud(5), Fields::ALL);
let bounds = Aabb {
min: [100.0, 100.0, 100.0],
max: [200.0, 200.0, 200.0],
};
assert!(chunk.indices_in_bounds(&bounds).unwrap().is_empty());
}
#[test]
fn indices_in_bounds_returns_none_without_z_field() {
let chunk = make_chunk(build_test_cloud(5), Fields::empty());
let bounds = Aabb {
min: [0.0, 0.0, 0.0],
max: [10.0, 10.0, 10.0],
};
assert!(chunk.indices_in_bounds(&bounds).is_none());
}
}