use crate::error::CopcError;
use crate::point::Point3D;
type BaseFields = (u8, u8, u8, i8, u8, u16, Option<f64>, Option<usize>);
pub fn min_record_size(format_id: u8) -> Result<usize, CopcError> {
match format_id {
0 => Ok(20),
1 => Ok(28),
2 => Ok(26),
3 => Ok(34),
6 => Ok(30),
7 => Ok(36),
8 => Ok(38),
other => Err(CopcError::InvalidFormat(format!(
"Unsupported point data format ID: {other}"
))),
}
}
#[inline]
fn is_extended_format(format_id: u8) -> bool {
format_id >= 6
}
pub fn deserialize_point(
record: &[u8],
format_id: u8,
scale: [f64; 3],
offset: [f64; 3],
) -> Result<Point3D, CopcError> {
let min_size = min_record_size(format_id)?;
if record.len() < min_size {
return Err(CopcError::InvalidFormat(format!(
"Point record too short for format {format_id}: {} bytes (need >= {min_size})",
record.len()
)));
}
let raw_x = i32::from_le_bytes([record[0], record[1], record[2], record[3]]);
let raw_y = i32::from_le_bytes([record[4], record[5], record[6], record[7]]);
let raw_z = i32::from_le_bytes([record[8], record[9], record[10], record[11]]);
let x = raw_x as f64 * scale[0] + offset[0];
let y = raw_y as f64 * scale[1] + offset[1];
let z = raw_z as f64 * scale[2] + offset[2];
let intensity = u16::from_le_bytes([record[12], record[13]]);
let (
return_number,
number_of_returns,
classification,
scan_angle_rank,
user_data,
point_source_id,
gps_time,
rgb_offset,
) = if is_extended_format(format_id) {
parse_extended_base(record, format_id)?
} else {
parse_legacy_base(record, format_id)?
};
let (red, green, blue) = if let Some(rgb_off) = rgb_offset {
if record.len() >= rgb_off + 6 {
let r = u16::from_le_bytes([record[rgb_off], record[rgb_off + 1]]);
let g = u16::from_le_bytes([record[rgb_off + 2], record[rgb_off + 3]]);
let b = u16::from_le_bytes([record[rgb_off + 4], record[rgb_off + 5]]);
(Some(r), Some(g), Some(b))
} else {
(None, None, None)
}
} else {
(None, None, None)
};
Ok(Point3D {
x,
y,
z,
intensity,
return_number,
number_of_returns,
classification,
scan_angle_rank,
user_data,
point_source_id,
gps_time,
red,
green,
blue,
})
}
fn parse_legacy_base(record: &[u8], format_id: u8) -> Result<BaseFields, CopcError> {
let packed = record[14];
let return_number = packed & 0x07;
let number_of_returns = (packed >> 3) & 0x07;
let classification = record[15];
let scan_angle_rank = record[16] as i8;
let user_data = record[17];
let point_source_id = u16::from_le_bytes([record[18], record[19]]);
let (gps_time, rgb_offset) = match format_id {
0 => (None, None),
1 => {
let gps = read_f64_le(record, 20)?;
(Some(gps), None)
}
2 => {
(None, Some(20))
}
3 => {
let gps = read_f64_le(record, 20)?;
(Some(gps), Some(28))
}
_ => {
return Err(CopcError::InvalidFormat(format!(
"Unsupported legacy format: {format_id}"
)));
}
};
Ok((
return_number,
number_of_returns,
classification,
scan_angle_rank,
user_data,
point_source_id,
gps_time,
rgb_offset,
))
}
fn parse_extended_base(record: &[u8], format_id: u8) -> Result<BaseFields, CopcError> {
let packed = record[14];
let return_number = packed & 0x0F;
let number_of_returns = (packed >> 4) & 0x0F;
let classification = record[16];
let user_data = record[17];
let raw_angle = i16::from_le_bytes([record[18], record[19]]);
let angle_degrees = raw_angle as f64 * 0.006;
let clamped = angle_degrees.round().clamp(-128.0, 127.0) as i8;
let point_source_id = u16::from_le_bytes([record[20], record[21]]);
let gps_time = read_f64_le(record, 22)?;
let rgb_offset = match format_id {
6 => None, 7 | 8 => Some(30), _ => {
return Err(CopcError::InvalidFormat(format!(
"Unsupported extended format: {format_id}"
)));
}
};
Ok((
return_number,
number_of_returns,
classification,
clamped,
user_data,
point_source_id,
Some(gps_time),
rgb_offset,
))
}
pub fn deserialize_points(
data: &[u8],
count: usize,
record_length: usize,
format_id: u8,
scale: [f64; 3],
offset: [f64; 3],
) -> Result<Vec<Point3D>, CopcError> {
let needed = count.checked_mul(record_length).ok_or_else(|| {
CopcError::InvalidFormat("Point count * record length overflows usize".into())
})?;
if data.len() < needed {
return Err(CopcError::InvalidFormat(format!(
"Point data too short: {} bytes (need {needed} for {count} records of {record_length} bytes)",
data.len()
)));
}
let mut points = Vec::with_capacity(count);
for i in 0..count {
let start = i * record_length;
let end = start + record_length;
let record = &data[start..end];
let point = deserialize_point(record, format_id, scale, offset)?;
points.push(point);
}
Ok(points)
}
#[inline]
fn read_f64_le(data: &[u8], off: usize) -> Result<f64, CopcError> {
if data.len() < off + 8 {
return Err(CopcError::InvalidFormat(format!(
"Cannot read f64 at offset {off}: data too short ({} bytes)",
data.len()
)));
}
Ok(f64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
]))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_format0_record(raw_x: i32, raw_y: i32, raw_z: i32, intensity: u16) -> 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(&intensity.to_le_bytes());
rec[14] = 2 | (3 << 3);
rec[15] = 2;
rec[16] = (-5i8) as u8;
rec[17] = 42;
rec[18..20].copy_from_slice(&7u16.to_le_bytes());
rec
}
fn make_format1_record(raw_x: i32, raw_y: i32, raw_z: i32, gps_time: f64) -> Vec<u8> {
let mut rec = make_format0_record(raw_x, raw_y, raw_z, 100);
rec.resize(28, 0);
rec[20..28].copy_from_slice(&gps_time.to_le_bytes());
rec
}
fn make_format2_record(raw_x: i32, raw_y: i32, raw_z: i32, r: u16, g: u16, b: u16) -> Vec<u8> {
let mut rec = make_format0_record(raw_x, raw_y, raw_z, 200);
rec.resize(26, 0);
rec[20..22].copy_from_slice(&r.to_le_bytes());
rec[22..24].copy_from_slice(&g.to_le_bytes());
rec[24..26].copy_from_slice(&b.to_le_bytes());
rec
}
fn make_format3_record(
raw_x: i32,
raw_y: i32,
raw_z: i32,
gps_time: f64,
r: u16,
g: u16,
b: u16,
) -> Vec<u8> {
let mut rec = make_format0_record(raw_x, raw_y, raw_z, 300);
rec.resize(34, 0);
rec[20..28].copy_from_slice(&gps_time.to_le_bytes());
rec[28..30].copy_from_slice(&r.to_le_bytes());
rec[30..32].copy_from_slice(&g.to_le_bytes());
rec[32..34].copy_from_slice(&b.to_le_bytes());
rec
}
fn make_format6_record(raw_x: i32, raw_y: i32, raw_z: i32, gps_time: f64) -> 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(&500u16.to_le_bytes()); rec[14] = 3 | (5 << 4);
rec[15] = 0;
rec[16] = 6;
rec[17] = 99;
rec[18..20].copy_from_slice(&5000i16.to_le_bytes());
rec[20..22].copy_from_slice(&12u16.to_le_bytes());
rec[22..30].copy_from_slice(&gps_time.to_le_bytes());
rec
}
fn make_format7_record(
raw_x: i32,
raw_y: i32,
raw_z: i32,
gps_time: f64,
r: u16,
g: u16,
b: u16,
) -> Vec<u8> {
let mut rec = make_format6_record(raw_x, raw_y, raw_z, gps_time);
rec.resize(36, 0);
rec[30..32].copy_from_slice(&r.to_le_bytes());
rec[32..34].copy_from_slice(&g.to_le_bytes());
rec[34..36].copy_from_slice(&b.to_le_bytes());
rec
}
fn make_format8_record(
raw_x: i32,
raw_y: i32,
raw_z: i32,
gps_time: f64,
r: u16,
g: u16,
b: u16,
) -> Vec<u8> {
let mut rec = make_format7_record(raw_x, raw_y, raw_z, gps_time, r, g, b);
rec.resize(38, 0);
rec[36..38].copy_from_slice(&1000u16.to_le_bytes()); rec
}
#[test]
fn test_format0_coordinates() {
let rec = make_format0_record(1000, 2000, 500, 150);
let scale = [0.001, 0.001, 0.001];
let offset = [0.0, 0.0, 0.0];
let pt = deserialize_point(&rec, 0, scale, offset).expect("format 0 parse");
assert!((pt.x - 1.0).abs() < 1e-9, "x = 1000 * 0.001 = 1.0");
assert!((pt.y - 2.0).abs() < 1e-9, "y = 2000 * 0.001 = 2.0");
assert!((pt.z - 0.5).abs() < 1e-9, "z = 500 * 0.001 = 0.5");
}
#[test]
fn test_format0_with_offset() {
let rec = make_format0_record(1000, 2000, 500, 150);
let scale = [0.001, 0.001, 0.001];
let offset = [100.0, 200.0, 50.0];
let pt = deserialize_point(&rec, 0, scale, offset).expect("format 0 with offset");
assert!((pt.x - 101.0).abs() < 1e-9);
assert!((pt.y - 202.0).abs() < 1e-9);
assert!((pt.z - 50.5).abs() < 1e-9);
}
#[test]
fn test_format0_intensity() {
let rec = make_format0_record(0, 0, 0, 150);
let pt = deserialize_point(&rec, 0, [1.0; 3], [0.0; 3]).expect("format 0");
assert_eq!(pt.intensity, 150);
}
#[test]
fn test_format0_return_number() {
let rec = make_format0_record(0, 0, 0, 0);
let pt = deserialize_point(&rec, 0, [1.0; 3], [0.0; 3]).expect("format 0");
assert_eq!(pt.return_number, 2);
assert_eq!(pt.number_of_returns, 3);
}
#[test]
fn test_format0_classification() {
let rec = make_format0_record(0, 0, 0, 0);
let pt = deserialize_point(&rec, 0, [1.0; 3], [0.0; 3]).expect("format 0");
assert_eq!(pt.classification, 2);
}
#[test]
fn test_format0_scan_angle() {
let rec = make_format0_record(0, 0, 0, 0);
let pt = deserialize_point(&rec, 0, [1.0; 3], [0.0; 3]).expect("format 0");
assert_eq!(pt.scan_angle_rank, -5);
}
#[test]
fn test_format0_user_data() {
let rec = make_format0_record(0, 0, 0, 0);
let pt = deserialize_point(&rec, 0, [1.0; 3], [0.0; 3]).expect("format 0");
assert_eq!(pt.user_data, 42);
}
#[test]
fn test_format0_point_source_id() {
let rec = make_format0_record(0, 0, 0, 0);
let pt = deserialize_point(&rec, 0, [1.0; 3], [0.0; 3]).expect("format 0");
assert_eq!(pt.point_source_id, 7);
}
#[test]
fn test_format0_no_gps_no_color() {
let rec = make_format0_record(0, 0, 0, 0);
let pt = deserialize_point(&rec, 0, [1.0; 3], [0.0; 3]).expect("format 0");
assert!(pt.gps_time.is_none());
assert!(pt.red.is_none());
assert!(pt.green.is_none());
assert!(pt.blue.is_none());
}
#[test]
fn test_format0_too_short() {
let rec = vec![0u8; 19]; assert!(deserialize_point(&rec, 0, [1.0; 3], [0.0; 3]).is_err());
}
#[test]
fn test_format1_gps_time() {
let rec = make_format1_record(0, 0, 0, 12345.678);
let pt = deserialize_point(&rec, 1, [1.0; 3], [0.0; 3]).expect("format 1");
let gps = pt.gps_time.expect("format 1 should have GPS time");
assert!((gps - 12345.678).abs() < 1e-6);
}
#[test]
fn test_format1_no_color() {
let rec = make_format1_record(0, 0, 0, 0.0);
let pt = deserialize_point(&rec, 1, [1.0; 3], [0.0; 3]).expect("format 1");
assert!(pt.red.is_none());
}
#[test]
fn test_format2_rgb() {
let rec = make_format2_record(0, 0, 0, 255, 128, 64);
let pt = deserialize_point(&rec, 2, [1.0; 3], [0.0; 3]).expect("format 2");
assert_eq!(pt.red, Some(255));
assert_eq!(pt.green, Some(128));
assert_eq!(pt.blue, Some(64));
}
#[test]
fn test_format2_no_gps() {
let rec = make_format2_record(0, 0, 0, 0, 0, 0);
let pt = deserialize_point(&rec, 2, [1.0; 3], [0.0; 3]).expect("format 2");
assert!(pt.gps_time.is_none());
}
#[test]
fn test_format3_gps_and_rgb() {
let rec = make_format3_record(1000, 2000, 3000, 99.9, 1000, 2000, 3000);
let pt = deserialize_point(&rec, 3, [0.01, 0.01, 0.01], [0.0; 3]).expect("format 3");
assert!((pt.x - 10.0).abs() < 1e-9);
let gps = pt.gps_time.expect("format 3 should have GPS");
assert!((gps - 99.9).abs() < 1e-6);
assert_eq!(pt.red, Some(1000));
assert_eq!(pt.green, Some(2000));
assert_eq!(pt.blue, Some(3000));
}
#[test]
fn test_format6_extended_base() {
let rec = make_format6_record(10000, 20000, 5000, 42.5);
let pt = deserialize_point(&rec, 6, [0.001, 0.001, 0.001], [0.0; 3]).expect("format 6");
assert!((pt.x - 10.0).abs() < 1e-9);
assert!((pt.y - 20.0).abs() < 1e-9);
assert!((pt.z - 5.0).abs() < 1e-9);
assert_eq!(pt.intensity, 500);
assert_eq!(pt.return_number, 3);
assert_eq!(pt.number_of_returns, 5);
assert_eq!(pt.classification, 6);
assert_eq!(pt.user_data, 99);
assert_eq!(pt.point_source_id, 12);
let gps = pt.gps_time.expect("format 6 always has GPS");
assert!((gps - 42.5).abs() < 1e-9);
}
#[test]
fn test_format6_scan_angle_conversion() {
let rec = make_format6_record(0, 0, 0, 0.0);
let pt = deserialize_point(&rec, 6, [1.0; 3], [0.0; 3]).expect("format 6");
assert_eq!(pt.scan_angle_rank, 30);
}
#[test]
fn test_format6_scan_angle_negative() {
let mut rec = make_format6_record(0, 0, 0, 0.0);
rec[18..20].copy_from_slice(&(-5000i16).to_le_bytes());
let pt = deserialize_point(&rec, 6, [1.0; 3], [0.0; 3]).expect("format 6 neg angle");
assert_eq!(pt.scan_angle_rank, -30);
}
#[test]
fn test_format6_scan_angle_clamped() {
let mut rec = make_format6_record(0, 0, 0, 0.0);
rec[18..20].copy_from_slice(&i16::MAX.to_le_bytes());
let pt = deserialize_point(&rec, 6, [1.0; 3], [0.0; 3]).expect("format 6 clamped");
assert_eq!(pt.scan_angle_rank, 127);
}
#[test]
fn test_format6_no_color() {
let rec = make_format6_record(0, 0, 0, 0.0);
let pt = deserialize_point(&rec, 6, [1.0; 3], [0.0; 3]).expect("format 6");
assert!(pt.red.is_none());
}
#[test]
fn test_format7_gps_and_rgb() {
let rec = make_format7_record(1000, 2000, 3000, 77.7, 500, 600, 700);
let pt = deserialize_point(&rec, 7, [0.001; 3], [0.0; 3]).expect("format 7");
let gps = pt.gps_time.expect("format 7 always has GPS");
assert!((gps - 77.7).abs() < 1e-9);
assert_eq!(pt.red, Some(500));
assert_eq!(pt.green, Some(600));
assert_eq!(pt.blue, Some(700));
}
#[test]
fn test_format8_gps_rgb_nir_ignored() {
let rec = make_format8_record(0, 0, 0, 55.5, 11, 22, 33);
let pt = deserialize_point(&rec, 8, [1.0; 3], [0.0; 3]).expect("format 8");
let gps = pt.gps_time.expect("format 8 always has GPS");
assert!((gps - 55.5).abs() < 1e-9);
assert_eq!(pt.red, Some(11));
assert_eq!(pt.green, Some(22));
assert_eq!(pt.blue, Some(33));
}
#[test]
fn test_min_record_size_all_formats() {
assert_eq!(min_record_size(0).expect("f0"), 20);
assert_eq!(min_record_size(1).expect("f1"), 28);
assert_eq!(min_record_size(2).expect("f2"), 26);
assert_eq!(min_record_size(3).expect("f3"), 34);
assert_eq!(min_record_size(6).expect("f6"), 30);
assert_eq!(min_record_size(7).expect("f7"), 36);
assert_eq!(min_record_size(8).expect("f8"), 38);
}
#[test]
fn test_min_record_size_unsupported() {
assert!(min_record_size(4).is_err());
assert!(min_record_size(5).is_err());
assert!(min_record_size(9).is_err());
assert!(min_record_size(255).is_err());
}
#[test]
fn test_deserialize_points_batch() {
let rec1 = make_format0_record(1000, 2000, 3000, 100);
let rec2 = make_format0_record(4000, 5000, 6000, 200);
let mut data = rec1;
data.extend_from_slice(&rec2);
let pts = deserialize_points(&data, 2, 20, 0, [0.001; 3], [0.0; 3]).expect("batch");
assert_eq!(pts.len(), 2);
assert!((pts[0].x - 1.0).abs() < 1e-9);
assert!((pts[1].x - 4.0).abs() < 1e-9);
}
#[test]
fn test_deserialize_points_batch_too_short() {
let data = vec![0u8; 30]; assert!(deserialize_points(&data, 2, 20, 0, [1.0; 3], [0.0; 3]).is_err());
}
#[test]
fn test_deserialize_points_zero_count() {
let pts = deserialize_points(&[], 0, 20, 0, [1.0; 3], [0.0; 3]).expect("zero");
assert!(pts.is_empty());
}
#[test]
fn test_format0_negative_coordinates() {
let rec = make_format0_record(-5000, -10000, -500, 0);
let pt = deserialize_point(&rec, 0, [0.001; 3], [0.0; 3]).expect("negative coords");
assert!((pt.x - (-5.0)).abs() < 1e-9);
assert!((pt.y - (-10.0)).abs() < 1e-9);
assert!((pt.z - (-0.5)).abs() < 1e-9);
}
#[test]
fn test_unsupported_format_id() {
let rec = vec![0u8; 50];
assert!(deserialize_point(&rec, 4, [1.0; 3], [0.0; 3]).is_err());
assert!(deserialize_point(&rec, 5, [1.0; 3], [0.0; 3]).is_err());
assert!(deserialize_point(&rec, 10, [1.0; 3], [0.0; 3]).is_err());
}
#[test]
fn test_format6_extended_return_numbers_up_to_15() {
let mut rec = make_format6_record(0, 0, 0, 0.0);
rec[14] = 15 | (15 << 4);
let pt = deserialize_point(&rec, 6, [1.0; 3], [0.0; 3]).expect("format 6 max returns");
assert_eq!(pt.return_number, 15);
assert_eq!(pt.number_of_returns, 15);
}
#[test]
fn test_deserialize_points_with_record_length_padding() {
let mut data = make_format0_record(1000, 0, 0, 0);
data.extend_from_slice(&[0u8; 4]); let mut data2 = make_format0_record(2000, 0, 0, 0);
data2.extend_from_slice(&[0u8; 4]); data.extend_from_slice(&data2);
let pts =
deserialize_points(&data, 2, 24, 0, [0.001; 3], [0.0; 3]).expect("padded records");
assert_eq!(pts.len(), 2);
assert!((pts[0].x - 1.0).abs() < 1e-9);
assert!((pts[1].x - 2.0).abs() < 1e-9);
}
}