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