use crate::las::header::PointDataFormat;
use crate::point::{GpsTime, PointRecord, Rgb16};
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct RawPoint10 {
pub xi: i32,
pub yi: i32,
pub zi: i32,
pub intensity: u16,
pub return_flags: u8,
pub class_flags: u8,
pub user_data: u8,
pub scan_angle_rank: i8,
pub point_source_id: u16,
pub gps_time: Option<f64>,
pub rgb: Option<Rgb16>,
}
impl RawPoint10 {
pub fn from_bytes(raw: &[u8], format: PointDataFormat) -> Option<Self> {
let expected = match format {
PointDataFormat::Pdrf0 => 20,
PointDataFormat::Pdrf1 => 28,
PointDataFormat::Pdrf2 => 26,
PointDataFormat::Pdrf3 => 34,
_ => return None,
};
if raw.len() < expected {
return None;
}
let mut out = Self {
xi: i32::from_le_bytes(raw[0..4].try_into().ok()?),
yi: i32::from_le_bytes(raw[4..8].try_into().ok()?),
zi: i32::from_le_bytes(raw[8..12].try_into().ok()?),
intensity: u16::from_le_bytes(raw[12..14].try_into().ok()?),
return_flags: raw[14],
class_flags: raw[15],
user_data: raw[16],
scan_angle_rank: raw[17] as i8,
point_source_id: u16::from_le_bytes(raw[18..20].try_into().ok()?),
gps_time: None,
rgb: None,
};
match format {
PointDataFormat::Pdrf1 => {
out.gps_time = Some(f64::from_le_bytes(raw[20..28].try_into().ok()?));
}
PointDataFormat::Pdrf2 => {
out.rgb = Some(Rgb16 {
red: u16::from_le_bytes(raw[20..22].try_into().ok()?),
green: u16::from_le_bytes(raw[22..24].try_into().ok()?),
blue: u16::from_le_bytes(raw[24..26].try_into().ok()?),
});
}
PointDataFormat::Pdrf3 => {
out.gps_time = Some(f64::from_le_bytes(raw[20..28].try_into().ok()?));
out.rgb = Some(Rgb16 {
red: u16::from_le_bytes(raw[28..30].try_into().ok()?),
green: u16::from_le_bytes(raw[30..32].try_into().ok()?),
blue: u16::from_le_bytes(raw[32..34].try_into().ok()?),
});
}
_ => {}
}
Some(out)
}
pub fn to_point_record(self, scales: [f64; 3], offsets: [f64; 3]) -> PointRecord {
PointRecord {
x: self.xi as f64 * scales[0] + offsets[0],
y: self.yi as f64 * scales[1] + offsets[1],
z: self.zi as f64 * scales[2] + offsets[2],
intensity: self.intensity,
color: self.rgb,
classification: self.class_flags & 0x1F,
user_data: self.user_data,
point_source_id: self.point_source_id,
flags: (self.class_flags >> 5) & 0x07,
return_number: self.return_flags & 0x07,
number_of_returns: (self.return_flags >> 3) & 0x07,
scan_direction_flag: (self.return_flags & 0x40) != 0,
edge_of_flight_line: (self.return_flags & 0x80) != 0,
scan_angle: self.scan_angle_rank as i16,
gps_time: self.gps_time.map(GpsTime),
..PointRecord::default()
}
}
pub fn from_point_record(
p: PointRecord,
format: PointDataFormat,
scales: [f64; 3],
offsets: [f64; 3],
) -> Option<Self> {
match format {
PointDataFormat::Pdrf0
| PointDataFormat::Pdrf1
| PointDataFormat::Pdrf2
| PointDataFormat::Pdrf3 => {}
_ => return None,
}
let return_flags = (p.return_number & 0x07)
| ((p.number_of_returns & 0x07) << 3)
| ((u8::from(p.scan_direction_flag)) << 6)
| ((u8::from(p.edge_of_flight_line)) << 7);
let class_flags = (p.classification & 0x1F) | ((p.flags & 0x07) << 5);
let out = Self {
xi: ((p.x - offsets[0]) / scales[0]).round() as i32,
yi: ((p.y - offsets[1]) / scales[1]).round() as i32,
zi: ((p.z - offsets[2]) / scales[2]).round() as i32,
intensity: p.intensity,
return_flags,
class_flags,
user_data: p.user_data,
scan_angle_rank: p.scan_angle as i8,
point_source_id: p.point_source_id,
gps_time: p.gps_time.map(|v| v.0),
rgb: p.color,
};
if matches!(format, PointDataFormat::Pdrf1 | PointDataFormat::Pdrf3) && out.gps_time.is_none() {
return None;
}
if matches!(format, PointDataFormat::Pdrf2 | PointDataFormat::Pdrf3) && out.rgb.is_none() {
return None;
}
Some(out)
}
}
#[cfg(test)]
mod tests {
use crate::las::header::PointDataFormat;
use crate::point::{GpsTime, PointRecord, Rgb16};
use super::RawPoint10;
#[test]
fn raw_point10_pdrf3_round_trip() {
let p = PointRecord {
x: 250.0,
y: 100.0,
z: 12.0,
intensity: 512,
color: Some(Rgb16 { red: 12, green: 34, blue: 56 }),
classification: 2,
user_data: 7,
point_source_id: 9,
flags: 0x03,
return_number: 1,
number_of_returns: 2,
scan_direction_flag: true,
edge_of_flight_line: false,
scan_angle: -12,
gps_time: Some(GpsTime(42.5)),
..PointRecord::default()
};
let scales = [0.01, 0.01, 0.01];
let offsets = [0.0, 0.0, 0.0];
let raw = RawPoint10::from_point_record(p, PointDataFormat::Pdrf3, scales, offsets)
.expect("raw conversion should succeed");
let bytes = {
let mut b = vec![0u8; 34];
b[0..4].copy_from_slice(&raw.xi.to_le_bytes());
b[4..8].copy_from_slice(&raw.yi.to_le_bytes());
b[8..12].copy_from_slice(&raw.zi.to_le_bytes());
b[12..14].copy_from_slice(&raw.intensity.to_le_bytes());
b[14] = raw.return_flags;
b[15] = raw.class_flags;
b[16] = raw.user_data;
b[17] = raw.scan_angle_rank as u8;
b[18..20].copy_from_slice(&raw.point_source_id.to_le_bytes());
b[20..28].copy_from_slice(&raw.gps_time.expect("gps").to_le_bytes());
let rgb = raw.rgb.expect("rgb");
b[28..30].copy_from_slice(&rgb.red.to_le_bytes());
b[30..32].copy_from_slice(&rgb.green.to_le_bytes());
b[32..34].copy_from_slice(&rgb.blue.to_le_bytes());
b
};
let decoded_raw = RawPoint10::from_bytes(&bytes, PointDataFormat::Pdrf3)
.expect("raw bytes should parse");
let decoded = decoded_raw.to_point_record(scales, offsets);
assert_eq!(decoded.intensity, p.intensity);
assert_eq!(decoded.classification, p.classification);
assert_eq!(decoded.color.expect("color").green, 34);
assert_eq!(decoded.gps_time.expect("gps").0, 42.5);
assert_eq!(decoded.return_number, p.return_number);
assert_eq!(decoded.number_of_returns, p.number_of_returns);
}
}