Skip to main content

copc_core/
streaming.rs

1//! Streaming LAS point record plus explicit little-endian spill bytes.
2
3use std::io;
4
5use las::point::Format as LasFormat;
6
7/// In-memory representation of one full-fidelity LAS point.
8#[derive(Clone, Debug, PartialEq)]
9pub struct LasPointRecord {
10    pub x: f64,
11    pub y: f64,
12    pub z: f64,
13    pub intensity: u16,
14    pub return_number: u8,
15    pub number_of_returns: u8,
16    pub classification: u8,
17    pub scan_direction_flag: bool,
18    pub edge_of_flight_line: bool,
19    pub scan_angle: i16,
20    pub user_data: u8,
21    pub point_source_id: u16,
22    pub synthetic: bool,
23    pub key_point: bool,
24    pub withheld: bool,
25    pub overlap: bool,
26    pub scan_channel: u8,
27    pub gps_time: f64,
28    pub red: u16,
29    pub green: u16,
30    pub blue: u16,
31    pub nir: u16,
32    pub wave_packet_descriptor_index: u8,
33    pub byte_offset_to_waveform_data: u64,
34    pub waveform_packet_size: u32,
35    pub return_point_waveform_location: f32,
36}
37
38impl LasPointRecord {
39    /// Convert from the canonical `las::Point` shape used by the `las` crate.
40    pub fn from_las_point(point: &las::Point) -> Self {
41        let scan_direction_flag =
42            matches!(point.scan_direction, las::point::ScanDirection::LeftToRight);
43        let scan_angle_scaled = (point.scan_angle * 180.0 / 90.0) as i32;
44        let scan_angle = scan_angle_scaled.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
45        let (red, green, blue) = match point.color {
46            Some(color) => (color.red, color.green, color.blue),
47            None => (32_768, 32_768, 32_768),
48        };
49        let (
50            wave_packet_descriptor_index,
51            byte_offset_to_waveform_data,
52            waveform_packet_size,
53            return_point_waveform_location,
54        ) = match point.waveform.as_ref() {
55            Some(wf) => (
56                wf.wave_packet_descriptor_index,
57                wf.byte_offset_to_waveform_data,
58                wf.waveform_packet_size_in_bytes,
59                wf.return_point_waveform_location,
60            ),
61            None => (0, 0, 0, 0.0),
62        };
63        Self {
64            x: point.x,
65            y: point.y,
66            z: point.z,
67            intensity: point.intensity,
68            return_number: point.return_number,
69            number_of_returns: point.number_of_returns,
70            classification: u8::from(point.classification),
71            scan_direction_flag,
72            edge_of_flight_line: point.is_edge_of_flight_line,
73            scan_angle,
74            user_data: point.user_data,
75            point_source_id: point.point_source_id,
76            synthetic: point.is_synthetic,
77            key_point: point.is_key_point,
78            withheld: point.is_withheld,
79            overlap: point.is_overlap,
80            scan_channel: point.scanner_channel,
81            gps_time: point.gps_time.unwrap_or(0.0),
82            red,
83            green,
84            blue,
85            nir: point.nir.unwrap_or(0),
86            wave_packet_descriptor_index,
87            byte_offset_to_waveform_data,
88            waveform_packet_size,
89            return_point_waveform_location,
90        }
91    }
92}
93
94/// Records which optional dimensions are present in a streaming pass.
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub struct StreamingLayout {
97    pub point_format: u8,
98    pub has_gps: bool,
99    pub has_color: bool,
100    pub has_nir: bool,
101    pub has_waveform: bool,
102}
103
104impl StreamingLayout {
105    pub fn from_las_format(format: LasFormat) -> Self {
106        Self {
107            point_format: format.to_u8().unwrap_or(0),
108            has_gps: format.has_gps_time,
109            has_color: format.has_color,
110            has_nir: format.has_nir,
111            has_waveform: format.has_waveform,
112        }
113    }
114
115    /// Compute the spill width in bytes per record.
116    pub const fn record_width(&self) -> usize {
117        let mut width = ALWAYS_BYTES;
118        if self.has_gps {
119            width += GPS_BYTES;
120        }
121        if self.has_color {
122            width += COLOR_BYTES;
123        }
124        if self.has_nir {
125            width += NIR_BYTES;
126        }
127        if self.has_waveform {
128            width += WAVEFORM_BYTES;
129        }
130        width
131    }
132
133    pub const fn max_record_width() -> usize {
134        ALWAYS_BYTES + GPS_BYTES + COLOR_BYTES + NIR_BYTES + WAVEFORM_BYTES
135    }
136}
137
138const ALWAYS_BYTES: usize = 8 + 8 + 8 + 2 + 1 + 1 + 1 + 1 + 1 + 2 + 1 + 2 + 1 + 1 + 1 + 1 + 1;
139const GPS_BYTES: usize = 8;
140const COLOR_BYTES: usize = 6;
141const NIR_BYTES: usize = 2;
142const WAVEFORM_BYTES: usize = 1 + 8 + 4 + 4;
143
144/// Serialize one record into `dst` using the fixed little-endian spill format.
145pub fn serialize_le(record: &LasPointRecord, layout: &StreamingLayout, dst: &mut [u8]) {
146    debug_assert_eq!(dst.len(), layout.record_width());
147    let mut offset = 0;
148
149    write_f64(&mut offset, dst, record.x);
150    write_f64(&mut offset, dst, record.y);
151    write_f64(&mut offset, dst, record.z);
152    write_u16(&mut offset, dst, record.intensity);
153    write_u8(&mut offset, dst, record.return_number);
154    write_u8(&mut offset, dst, record.number_of_returns);
155    write_u8(&mut offset, dst, record.classification);
156    write_u8(&mut offset, dst, u8::from(record.scan_direction_flag));
157    write_u8(&mut offset, dst, u8::from(record.edge_of_flight_line));
158    write_i16(&mut offset, dst, record.scan_angle);
159    write_u8(&mut offset, dst, record.user_data);
160    write_u16(&mut offset, dst, record.point_source_id);
161    write_u8(&mut offset, dst, u8::from(record.synthetic));
162    write_u8(&mut offset, dst, u8::from(record.key_point));
163    write_u8(&mut offset, dst, u8::from(record.withheld));
164    write_u8(&mut offset, dst, u8::from(record.overlap));
165    write_u8(&mut offset, dst, record.scan_channel);
166
167    if layout.has_gps {
168        write_f64(&mut offset, dst, record.gps_time);
169    }
170    if layout.has_color {
171        write_u16(&mut offset, dst, record.red);
172        write_u16(&mut offset, dst, record.green);
173        write_u16(&mut offset, dst, record.blue);
174    }
175    if layout.has_nir {
176        write_u16(&mut offset, dst, record.nir);
177    }
178    if layout.has_waveform {
179        write_u8(&mut offset, dst, record.wave_packet_descriptor_index);
180        write_u64(&mut offset, dst, record.byte_offset_to_waveform_data);
181        write_u32(&mut offset, dst, record.waveform_packet_size);
182        write_f32(&mut offset, dst, record.return_point_waveform_location);
183    }
184    debug_assert_eq!(offset, layout.record_width());
185}
186
187/// Deserialize one record from the little-endian spill bytes.
188pub fn deserialize_le(src: &[u8], layout: &StreamingLayout) -> io::Result<LasPointRecord> {
189    if src.len() != layout.record_width() {
190        return Err(io::Error::new(
191            io::ErrorKind::InvalidData,
192            format!(
193                "record is {} bytes, expected {}",
194                src.len(),
195                layout.record_width()
196            ),
197        ));
198    }
199    let mut offset = 0;
200    let x = read_f64(&mut offset, src);
201    let y = read_f64(&mut offset, src);
202    let z = read_f64(&mut offset, src);
203    let intensity = read_u16(&mut offset, src);
204    let return_number = read_u8(&mut offset, src);
205    let number_of_returns = read_u8(&mut offset, src);
206    let classification = read_u8(&mut offset, src);
207    let scan_direction_flag = read_u8(&mut offset, src) != 0;
208    let edge_of_flight_line = read_u8(&mut offset, src) != 0;
209    let scan_angle = read_i16(&mut offset, src);
210    let user_data = read_u8(&mut offset, src);
211    let point_source_id = read_u16(&mut offset, src);
212    let synthetic = read_u8(&mut offset, src) != 0;
213    let key_point = read_u8(&mut offset, src) != 0;
214    let withheld = read_u8(&mut offset, src) != 0;
215    let overlap = read_u8(&mut offset, src) != 0;
216    let scan_channel = read_u8(&mut offset, src);
217    let gps_time = if layout.has_gps {
218        read_f64(&mut offset, src)
219    } else {
220        0.0
221    };
222    let (red, green, blue) = if layout.has_color {
223        (
224            read_u16(&mut offset, src),
225            read_u16(&mut offset, src),
226            read_u16(&mut offset, src),
227        )
228    } else {
229        (0, 0, 0)
230    };
231    let nir = if layout.has_nir {
232        read_u16(&mut offset, src)
233    } else {
234        0
235    };
236    let (
237        wave_packet_descriptor_index,
238        byte_offset_to_waveform_data,
239        waveform_packet_size,
240        return_point_waveform_location,
241    ) = if layout.has_waveform {
242        (
243            read_u8(&mut offset, src),
244            read_u64(&mut offset, src),
245            read_u32(&mut offset, src),
246            read_f32(&mut offset, src),
247        )
248    } else {
249        (0, 0, 0, 0.0)
250    };
251    debug_assert_eq!(offset, layout.record_width());
252    Ok(LasPointRecord {
253        x,
254        y,
255        z,
256        intensity,
257        return_number,
258        number_of_returns,
259        classification,
260        scan_direction_flag,
261        edge_of_flight_line,
262        scan_angle,
263        user_data,
264        point_source_id,
265        synthetic,
266        key_point,
267        withheld,
268        overlap,
269        scan_channel,
270        gps_time,
271        red,
272        green,
273        blue,
274        nir,
275        wave_packet_descriptor_index,
276        byte_offset_to_waveform_data,
277        waveform_packet_size,
278        return_point_waveform_location,
279    })
280}
281
282#[inline]
283fn write_u8(offset: &mut usize, dst: &mut [u8], value: u8) {
284    dst[*offset] = value;
285    *offset += 1;
286}
287
288#[inline]
289fn write_u16(offset: &mut usize, dst: &mut [u8], value: u16) {
290    dst[*offset..*offset + 2].copy_from_slice(&value.to_le_bytes());
291    *offset += 2;
292}
293
294#[inline]
295fn write_i16(offset: &mut usize, dst: &mut [u8], value: i16) {
296    dst[*offset..*offset + 2].copy_from_slice(&value.to_le_bytes());
297    *offset += 2;
298}
299
300#[inline]
301fn write_u32(offset: &mut usize, dst: &mut [u8], value: u32) {
302    dst[*offset..*offset + 4].copy_from_slice(&value.to_le_bytes());
303    *offset += 4;
304}
305
306#[inline]
307fn write_u64(offset: &mut usize, dst: &mut [u8], value: u64) {
308    dst[*offset..*offset + 8].copy_from_slice(&value.to_le_bytes());
309    *offset += 8;
310}
311
312#[inline]
313fn write_f32(offset: &mut usize, dst: &mut [u8], value: f32) {
314    dst[*offset..*offset + 4].copy_from_slice(&value.to_le_bytes());
315    *offset += 4;
316}
317
318#[inline]
319fn write_f64(offset: &mut usize, dst: &mut [u8], value: f64) {
320    dst[*offset..*offset + 8].copy_from_slice(&value.to_le_bytes());
321    *offset += 8;
322}
323
324#[inline]
325fn read_u8(offset: &mut usize, src: &[u8]) -> u8 {
326    let value = src[*offset];
327    *offset += 1;
328    value
329}
330
331#[inline]
332fn read_u16(offset: &mut usize, src: &[u8]) -> u16 {
333    let value = u16::from_le_bytes(src[*offset..*offset + 2].try_into().expect("u16 width"));
334    *offset += 2;
335    value
336}
337
338#[inline]
339fn read_i16(offset: &mut usize, src: &[u8]) -> i16 {
340    let value = i16::from_le_bytes(src[*offset..*offset + 2].try_into().expect("i16 width"));
341    *offset += 2;
342    value
343}
344
345#[inline]
346fn read_u32(offset: &mut usize, src: &[u8]) -> u32 {
347    let value = u32::from_le_bytes(src[*offset..*offset + 4].try_into().expect("u32 width"));
348    *offset += 4;
349    value
350}
351
352#[inline]
353fn read_u64(offset: &mut usize, src: &[u8]) -> u64 {
354    let value = u64::from_le_bytes(src[*offset..*offset + 8].try_into().expect("u64 width"));
355    *offset += 8;
356    value
357}
358
359#[inline]
360fn read_f32(offset: &mut usize, src: &[u8]) -> f32 {
361    let value = f32::from_le_bytes(src[*offset..*offset + 4].try_into().expect("f32 width"));
362    *offset += 4;
363    value
364}
365
366#[inline]
367fn read_f64(offset: &mut usize, src: &[u8]) -> f64 {
368    let value = f64::from_le_bytes(src[*offset..*offset + 8].try_into().expect("f64 width"));
369    *offset += 8;
370    value
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    fn fixture_record() -> LasPointRecord {
378        LasPointRecord {
379            x: 1234.5678,
380            y: -9876.54321,
381            z: 100.0,
382            intensity: 0xBEEF,
383            return_number: 3,
384            number_of_returns: 5,
385            classification: 7,
386            scan_direction_flag: true,
387            edge_of_flight_line: true,
388            scan_angle: -1234,
389            user_data: 0x42,
390            point_source_id: 0xCAFE,
391            synthetic: true,
392            key_point: false,
393            withheld: true,
394            overlap: false,
395            scan_channel: 2,
396            gps_time: 1.234e9,
397            red: 0xAAAA,
398            green: 0x5555,
399            blue: 0xF00F,
400            nir: 0xCDCD,
401            wave_packet_descriptor_index: 9,
402            byte_offset_to_waveform_data: 0xDEADBEEF,
403            waveform_packet_size: 0xABCD,
404            return_point_waveform_location: -42.5,
405        }
406    }
407
408    #[test]
409    fn round_trip_every_optional_combination() {
410        let template = fixture_record();
411        for has_gps in [false, true] {
412            for has_color in [false, true] {
413                for has_nir in [false, true] {
414                    for has_waveform in [false, true] {
415                        let layout = StreamingLayout {
416                            point_format: 10,
417                            has_gps,
418                            has_color,
419                            has_nir,
420                            has_waveform,
421                        };
422                        let mut record = template.clone();
423                        if !layout.has_gps {
424                            record.gps_time = 0.0;
425                        }
426                        if !layout.has_color {
427                            record.red = 0;
428                            record.green = 0;
429                            record.blue = 0;
430                        }
431                        if !layout.has_nir {
432                            record.nir = 0;
433                        }
434                        if !layout.has_waveform {
435                            record.wave_packet_descriptor_index = 0;
436                            record.byte_offset_to_waveform_data = 0;
437                            record.waveform_packet_size = 0;
438                            record.return_point_waveform_location = 0.0;
439                        }
440                        let mut bytes = vec![0u8; layout.record_width()];
441                        serialize_le(&record, &layout, &mut bytes);
442                        assert_eq!(deserialize_le(&bytes, &layout).unwrap(), record);
443                    }
444                }
445            }
446        }
447    }
448
449    #[test]
450    fn from_las_format_records_presence_flags() {
451        let layout0 = StreamingLayout::from_las_format(LasFormat::new(0).unwrap());
452        assert!(!layout0.has_gps);
453        assert!(!layout0.has_color);
454        assert!(!layout0.has_nir);
455        assert!(!layout0.has_waveform);
456
457        let layout3 = StreamingLayout::from_las_format(LasFormat::new(3).unwrap());
458        assert!(layout3.has_gps);
459        assert!(layout3.has_color);
460        assert!(!layout3.has_nir);
461        assert!(!layout3.has_waveform);
462
463        let layout10 = StreamingLayout::from_las_format(LasFormat::new(10).unwrap());
464        assert!(layout10.has_gps);
465        assert!(layout10.has_color);
466        assert!(layout10.has_nir);
467        assert!(layout10.has_waveform);
468    }
469}