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, Default, 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    let mut record = LasPointRecord::default();
243    deserialize_le_into(src, layout, &mut record)?;
244    Ok(record)
245}
246
247/// Deserialize one record into `out`, reusing its `extra_bytes` allocation.
248pub fn deserialize_le_into(
249    src: &[u8],
250    layout: &StreamingLayout,
251    out: &mut LasPointRecord,
252) -> io::Result<()> {
253    if src.len() != layout.record_width() {
254        return Err(io::Error::new(
255            io::ErrorKind::InvalidData,
256            format!(
257                "record is {} bytes, expected {}",
258                src.len(),
259                layout.record_width()
260            ),
261        ));
262    }
263    let mut offset = 0;
264    let x = read_f64(&mut offset, src);
265    let y = read_f64(&mut offset, src);
266    let z = read_f64(&mut offset, src);
267    let intensity = read_u16(&mut offset, src);
268    let return_number = read_u8(&mut offset, src);
269    let number_of_returns = read_u8(&mut offset, src);
270    let classification = read_u8(&mut offset, src);
271    let scan_direction_flag = read_u8(&mut offset, src) != 0;
272    let edge_of_flight_line = read_u8(&mut offset, src) != 0;
273    let scan_angle = read_f32(&mut offset, src);
274    let user_data = read_u8(&mut offset, src);
275    let point_source_id = read_u16(&mut offset, src);
276    let synthetic = read_u8(&mut offset, src) != 0;
277    let key_point = read_u8(&mut offset, src) != 0;
278    let withheld = read_u8(&mut offset, src) != 0;
279    let overlap = read_u8(&mut offset, src) != 0;
280    let scan_channel = read_u8(&mut offset, src);
281    let gps_time = if layout.has_gps {
282        read_f64(&mut offset, src)
283    } else {
284        0.0
285    };
286    let (red, green, blue) = if layout.has_color {
287        (
288            read_u16(&mut offset, src),
289            read_u16(&mut offset, src),
290            read_u16(&mut offset, src),
291        )
292    } else {
293        (0, 0, 0)
294    };
295    let nir = if layout.has_nir {
296        read_u16(&mut offset, src)
297    } else {
298        0
299    };
300    let (
301        wave_packet_descriptor_index,
302        byte_offset_to_waveform_data,
303        waveform_packet_size,
304        return_point_waveform_location,
305    ) = if layout.has_waveform {
306        (
307            read_u8(&mut offset, src),
308            read_u64(&mut offset, src),
309            read_u32(&mut offset, src),
310            read_f32(&mut offset, src),
311        )
312    } else {
313        (0, 0, 0, 0.0)
314    };
315    out.extra_bytes.clear();
316    if layout.extra_bytes > 0 {
317        let end = offset + usize::from(layout.extra_bytes);
318        out.extra_bytes.extend_from_slice(&src[offset..end]);
319        offset = end;
320    }
321    debug_assert_eq!(offset, layout.record_width());
322    out.x = x;
323    out.y = y;
324    out.z = z;
325    out.intensity = intensity;
326    out.return_number = return_number;
327    out.number_of_returns = number_of_returns;
328    out.classification = classification;
329    out.scan_direction_flag = scan_direction_flag;
330    out.edge_of_flight_line = edge_of_flight_line;
331    out.scan_angle = scan_angle;
332    out.user_data = user_data;
333    out.point_source_id = point_source_id;
334    out.synthetic = synthetic;
335    out.key_point = key_point;
336    out.withheld = withheld;
337    out.overlap = overlap;
338    out.scan_channel = scan_channel;
339    out.gps_time = gps_time;
340    out.red = red;
341    out.green = green;
342    out.blue = blue;
343    out.nir = nir;
344    out.wave_packet_descriptor_index = wave_packet_descriptor_index;
345    out.byte_offset_to_waveform_data = byte_offset_to_waveform_data;
346    out.waveform_packet_size = waveform_packet_size;
347    out.return_point_waveform_location = return_point_waveform_location;
348    Ok(())
349}
350
351#[inline]
352fn write_u8(offset: &mut usize, dst: &mut [u8], value: u8) {
353    dst[*offset] = value;
354    *offset += 1;
355}
356
357#[inline]
358fn write_u16(offset: &mut usize, dst: &mut [u8], value: u16) {
359    dst[*offset..*offset + 2].copy_from_slice(&value.to_le_bytes());
360    *offset += 2;
361}
362
363#[inline]
364fn write_u32(offset: &mut usize, dst: &mut [u8], value: u32) {
365    dst[*offset..*offset + 4].copy_from_slice(&value.to_le_bytes());
366    *offset += 4;
367}
368
369#[inline]
370fn write_u64(offset: &mut usize, dst: &mut [u8], value: u64) {
371    dst[*offset..*offset + 8].copy_from_slice(&value.to_le_bytes());
372    *offset += 8;
373}
374
375#[inline]
376fn write_f32(offset: &mut usize, dst: &mut [u8], value: f32) {
377    dst[*offset..*offset + 4].copy_from_slice(&value.to_le_bytes());
378    *offset += 4;
379}
380
381#[inline]
382fn write_f64(offset: &mut usize, dst: &mut [u8], value: f64) {
383    dst[*offset..*offset + 8].copy_from_slice(&value.to_le_bytes());
384    *offset += 8;
385}
386
387#[inline]
388fn read_u8(offset: &mut usize, src: &[u8]) -> u8 {
389    let value = src[*offset];
390    *offset += 1;
391    value
392}
393
394#[inline]
395fn read_u16(offset: &mut usize, src: &[u8]) -> u16 {
396    let value = u16::from_le_bytes(src[*offset..*offset + 2].try_into().expect("u16 width"));
397    *offset += 2;
398    value
399}
400
401#[inline]
402fn read_u32(offset: &mut usize, src: &[u8]) -> u32 {
403    let value = u32::from_le_bytes(src[*offset..*offset + 4].try_into().expect("u32 width"));
404    *offset += 4;
405    value
406}
407
408#[inline]
409fn read_u64(offset: &mut usize, src: &[u8]) -> u64 {
410    let value = u64::from_le_bytes(src[*offset..*offset + 8].try_into().expect("u64 width"));
411    *offset += 8;
412    value
413}
414
415#[inline]
416fn read_f32(offset: &mut usize, src: &[u8]) -> f32 {
417    let value = f32::from_le_bytes(src[*offset..*offset + 4].try_into().expect("f32 width"));
418    *offset += 4;
419    value
420}
421
422#[inline]
423fn read_f64(offset: &mut usize, src: &[u8]) -> f64 {
424    let value = f64::from_le_bytes(src[*offset..*offset + 8].try_into().expect("f64 width"));
425    *offset += 8;
426    value
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    fn fixture_record() -> LasPointRecord {
434        LasPointRecord {
435            x: 1234.5678,
436            y: -9876.54321,
437            z: 100.0,
438            intensity: 0xBEEF,
439            return_number: 3,
440            number_of_returns: 5,
441            classification: 7,
442            scan_direction_flag: true,
443            edge_of_flight_line: true,
444            scan_angle: -12.34,
445            user_data: 0x42,
446            point_source_id: 0xCAFE,
447            synthetic: true,
448            key_point: false,
449            withheld: true,
450            overlap: false,
451            scan_channel: 2,
452            gps_time: 1.234e9,
453            red: 0xAAAA,
454            green: 0x5555,
455            blue: 0xF00F,
456            nir: 0xCDCD,
457            wave_packet_descriptor_index: 9,
458            byte_offset_to_waveform_data: 0xDEADBEEF,
459            waveform_packet_size: 0xABCD,
460            return_point_waveform_location: -42.5,
461            extra_bytes: vec![0xA0, 0xB1, 0xC2],
462        }
463    }
464
465    #[test]
466    fn round_trip_every_optional_combination() {
467        let template = fixture_record();
468        for has_gps in [false, true] {
469            for has_color in [false, true] {
470                for has_nir in [false, true] {
471                    for has_waveform in [false, true] {
472                        let layout = StreamingLayout {
473                            point_format: 10,
474                            has_gps,
475                            has_color,
476                            has_nir,
477                            has_waveform,
478                            extra_bytes: 3,
479                            extra_bytes_descriptors: Vec::new(),
480                        };
481                        let mut record = template.clone();
482                        if !layout.has_gps {
483                            record.gps_time = 0.0;
484                        }
485                        if !layout.has_color {
486                            record.red = 0;
487                            record.green = 0;
488                            record.blue = 0;
489                        }
490                        if !layout.has_nir {
491                            record.nir = 0;
492                        }
493                        if !layout.has_waveform {
494                            record.wave_packet_descriptor_index = 0;
495                            record.byte_offset_to_waveform_data = 0;
496                            record.waveform_packet_size = 0;
497                            record.return_point_waveform_location = 0.0;
498                        }
499                        let mut bytes = vec![0u8; layout.record_width()];
500                        serialize_le(&record, &layout, &mut bytes).unwrap();
501                        assert_eq!(deserialize_le(&bytes, &layout).unwrap(), record);
502                    }
503                }
504            }
505        }
506    }
507
508    #[test]
509    fn from_las_point_preserves_fractional_scan_angle_degrees() {
510        let point = las::Point {
511            scan_angle: 30.25,
512            ..Default::default()
513        };
514
515        let record = LasPointRecord::from_las_point(&point);
516
517        assert_eq!(30.25, record.scan_angle);
518    }
519
520    #[test]
521    fn from_las_format_records_presence_flags() {
522        let layout0 = StreamingLayout::from_las_format(LasFormat::new(0).unwrap());
523        assert!(!layout0.has_gps);
524        assert!(!layout0.has_color);
525        assert!(!layout0.has_nir);
526        assert!(!layout0.has_waveform);
527        assert_eq!(0, layout0.extra_bytes);
528
529        let layout3 = StreamingLayout::from_las_format(LasFormat::new(3).unwrap());
530        assert!(layout3.has_gps);
531        assert!(layout3.has_color);
532        assert!(!layout3.has_nir);
533        assert!(!layout3.has_waveform);
534        assert_eq!(0, layout3.extra_bytes);
535
536        let mut format10 = LasFormat::new(10).unwrap();
537        format10.extra_bytes = 7;
538        let layout10 = StreamingLayout::from_las_format(format10);
539        assert!(layout10.has_gps);
540        assert!(layout10.has_color);
541        assert!(layout10.has_nir);
542        assert!(layout10.has_waveform);
543        assert_eq!(7, layout10.extra_bytes);
544    }
545}