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    /// Scan angle in degrees.
20    pub scan_angle: f32,
21    pub user_data: u8,
22    pub point_source_id: u16,
23    pub synthetic: bool,
24    pub key_point: bool,
25    pub withheld: bool,
26    pub overlap: bool,
27    pub scan_channel: u8,
28    pub gps_time: f64,
29    pub red: u16,
30    pub green: u16,
31    pub blue: u16,
32    pub nir: u16,
33    pub wave_packet_descriptor_index: u8,
34    pub byte_offset_to_waveform_data: u64,
35    pub waveform_packet_size: u32,
36    pub return_point_waveform_location: f32,
37}
38
39impl LasPointRecord {
40    /// Convert from the canonical `las::Point` shape used by the `las` crate.
41    pub fn from_las_point(point: &las::Point) -> Self {
42        let scan_direction_flag =
43            matches!(point.scan_direction, las::point::ScanDirection::LeftToRight);
44        let scan_angle = point.scan_angle;
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 + 4 + 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_f32(&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_f32(&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_u32(offset: &mut usize, dst: &mut [u8], value: u32) {
296    dst[*offset..*offset + 4].copy_from_slice(&value.to_le_bytes());
297    *offset += 4;
298}
299
300#[inline]
301fn write_u64(offset: &mut usize, dst: &mut [u8], value: u64) {
302    dst[*offset..*offset + 8].copy_from_slice(&value.to_le_bytes());
303    *offset += 8;
304}
305
306#[inline]
307fn write_f32(offset: &mut usize, dst: &mut [u8], value: f32) {
308    dst[*offset..*offset + 4].copy_from_slice(&value.to_le_bytes());
309    *offset += 4;
310}
311
312#[inline]
313fn write_f64(offset: &mut usize, dst: &mut [u8], value: f64) {
314    dst[*offset..*offset + 8].copy_from_slice(&value.to_le_bytes());
315    *offset += 8;
316}
317
318#[inline]
319fn read_u8(offset: &mut usize, src: &[u8]) -> u8 {
320    let value = src[*offset];
321    *offset += 1;
322    value
323}
324
325#[inline]
326fn read_u16(offset: &mut usize, src: &[u8]) -> u16 {
327    let value = u16::from_le_bytes(src[*offset..*offset + 2].try_into().expect("u16 width"));
328    *offset += 2;
329    value
330}
331
332#[inline]
333fn read_u32(offset: &mut usize, src: &[u8]) -> u32 {
334    let value = u32::from_le_bytes(src[*offset..*offset + 4].try_into().expect("u32 width"));
335    *offset += 4;
336    value
337}
338
339#[inline]
340fn read_u64(offset: &mut usize, src: &[u8]) -> u64 {
341    let value = u64::from_le_bytes(src[*offset..*offset + 8].try_into().expect("u64 width"));
342    *offset += 8;
343    value
344}
345
346#[inline]
347fn read_f32(offset: &mut usize, src: &[u8]) -> f32 {
348    let value = f32::from_le_bytes(src[*offset..*offset + 4].try_into().expect("f32 width"));
349    *offset += 4;
350    value
351}
352
353#[inline]
354fn read_f64(offset: &mut usize, src: &[u8]) -> f64 {
355    let value = f64::from_le_bytes(src[*offset..*offset + 8].try_into().expect("f64 width"));
356    *offset += 8;
357    value
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    fn fixture_record() -> LasPointRecord {
365        LasPointRecord {
366            x: 1234.5678,
367            y: -9876.54321,
368            z: 100.0,
369            intensity: 0xBEEF,
370            return_number: 3,
371            number_of_returns: 5,
372            classification: 7,
373            scan_direction_flag: true,
374            edge_of_flight_line: true,
375            scan_angle: -12.34,
376            user_data: 0x42,
377            point_source_id: 0xCAFE,
378            synthetic: true,
379            key_point: false,
380            withheld: true,
381            overlap: false,
382            scan_channel: 2,
383            gps_time: 1.234e9,
384            red: 0xAAAA,
385            green: 0x5555,
386            blue: 0xF00F,
387            nir: 0xCDCD,
388            wave_packet_descriptor_index: 9,
389            byte_offset_to_waveform_data: 0xDEADBEEF,
390            waveform_packet_size: 0xABCD,
391            return_point_waveform_location: -42.5,
392        }
393    }
394
395    #[test]
396    fn round_trip_every_optional_combination() {
397        let template = fixture_record();
398        for has_gps in [false, true] {
399            for has_color in [false, true] {
400                for has_nir in [false, true] {
401                    for has_waveform in [false, true] {
402                        let layout = StreamingLayout {
403                            point_format: 10,
404                            has_gps,
405                            has_color,
406                            has_nir,
407                            has_waveform,
408                        };
409                        let mut record = template.clone();
410                        if !layout.has_gps {
411                            record.gps_time = 0.0;
412                        }
413                        if !layout.has_color {
414                            record.red = 0;
415                            record.green = 0;
416                            record.blue = 0;
417                        }
418                        if !layout.has_nir {
419                            record.nir = 0;
420                        }
421                        if !layout.has_waveform {
422                            record.wave_packet_descriptor_index = 0;
423                            record.byte_offset_to_waveform_data = 0;
424                            record.waveform_packet_size = 0;
425                            record.return_point_waveform_location = 0.0;
426                        }
427                        let mut bytes = vec![0u8; layout.record_width()];
428                        serialize_le(&record, &layout, &mut bytes);
429                        assert_eq!(deserialize_le(&bytes, &layout).unwrap(), record);
430                    }
431                }
432            }
433        }
434    }
435
436    #[test]
437    fn from_las_point_preserves_fractional_scan_angle_degrees() {
438        let point = las::Point {
439            scan_angle: 30.25,
440            ..Default::default()
441        };
442
443        let record = LasPointRecord::from_las_point(&point);
444
445        assert_eq!(30.25, record.scan_angle);
446    }
447
448    #[test]
449    fn from_las_format_records_presence_flags() {
450        let layout0 = StreamingLayout::from_las_format(LasFormat::new(0).unwrap());
451        assert!(!layout0.has_gps);
452        assert!(!layout0.has_color);
453        assert!(!layout0.has_nir);
454        assert!(!layout0.has_waveform);
455
456        let layout3 = StreamingLayout::from_las_format(LasFormat::new(3).unwrap());
457        assert!(layout3.has_gps);
458        assert!(layout3.has_color);
459        assert!(!layout3.has_nir);
460        assert!(!layout3.has_waveform);
461
462        let layout10 = StreamingLayout::from_las_format(LasFormat::new(10).unwrap());
463        assert!(layout10.has_gps);
464        assert!(layout10.has_color);
465        assert!(layout10.has_nir);
466        assert!(layout10.has_waveform);
467    }
468}