Skip to main content

copc_core/
columns.rs

1//! Column-oriented LAS/COPC point data.
2
3use crate::{Error, Result};
4
5use las::point::Format as LasPointFormat;
6
7/// LAS/COPC point dimensions that can be represented as columns.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum LasDimension {
10    X,
11    Y,
12    Z,
13    Intensity,
14    ReturnNumber,
15    NumberOfReturns,
16    Classification,
17    ScanDirectionFlag,
18    EdgeOfFlightLine,
19    ScanAngleRank,
20    UserData,
21    PointSourceId,
22    Synthetic,
23    KeyPoint,
24    Withheld,
25    Overlap,
26    ScanChannel,
27    GpsTime,
28    Red,
29    Green,
30    Blue,
31    Nir,
32    WaveformPacketDescriptorIndex,
33    WaveformPacketByteOffset,
34    WaveformPacketSize,
35    WavePacketReturnPointWaveformLocation,
36    ExtraBytes,
37}
38
39impl LasDimension {
40    /// The default scalar representation for fixed LAS/COPC dimensions.
41    pub const fn default_scalar(self) -> Option<ScalarType> {
42        match self {
43            Self::X | Self::Y | Self::Z | Self::GpsTime => Some(ScalarType::F64),
44            Self::WavePacketReturnPointWaveformLocation => Some(ScalarType::F32),
45            Self::ScanAngleRank => Some(ScalarType::I16),
46            Self::WaveformPacketByteOffset => Some(ScalarType::U64),
47            Self::Intensity
48            | Self::PointSourceId
49            | Self::Red
50            | Self::Green
51            | Self::Blue
52            | Self::Nir => Some(ScalarType::U16),
53            Self::WaveformPacketSize => Some(ScalarType::U32),
54            Self::ReturnNumber
55            | Self::NumberOfReturns
56            | Self::Classification
57            | Self::UserData
58            | Self::ScanChannel
59            | Self::WaveformPacketDescriptorIndex => Some(ScalarType::U8),
60            Self::ScanDirectionFlag
61            | Self::EdgeOfFlightLine
62            | Self::Synthetic
63            | Self::KeyPoint
64            | Self::Withheld
65            | Self::Overlap => Some(ScalarType::Bool),
66            Self::ExtraBytes => None,
67        }
68    }
69
70    /// Returns whether `scalar` is the default fixed-width representation for this dimension.
71    pub const fn accepts_scalar(self, scalar: ScalarType) -> bool {
72        match self.default_scalar() {
73            Some(default) => default as u8 == scalar as u8,
74            None => true,
75        }
76    }
77}
78
79/// Requested LAS/COPC dimensions for column-oriented reads.
80#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct ColumnSelection {
82    dimensions: Vec<LasDimension>,
83}
84
85impl ColumnSelection {
86    pub fn all() -> Self {
87        Self::from_dimensions([
88            LasDimension::X,
89            LasDimension::Y,
90            LasDimension::Z,
91            LasDimension::Intensity,
92            LasDimension::ReturnNumber,
93            LasDimension::NumberOfReturns,
94            LasDimension::Classification,
95            LasDimension::ScanDirectionFlag,
96            LasDimension::EdgeOfFlightLine,
97            LasDimension::ScanAngleRank,
98            LasDimension::UserData,
99            LasDimension::PointSourceId,
100            LasDimension::Synthetic,
101            LasDimension::KeyPoint,
102            LasDimension::Withheld,
103            LasDimension::Overlap,
104            LasDimension::ScanChannel,
105            LasDimension::GpsTime,
106            LasDimension::Red,
107            LasDimension::Green,
108            LasDimension::Blue,
109            LasDimension::Nir,
110            LasDimension::WaveformPacketDescriptorIndex,
111            LasDimension::WaveformPacketByteOffset,
112            LasDimension::WaveformPacketSize,
113            LasDimension::WavePacketReturnPointWaveformLocation,
114            LasDimension::ExtraBytes,
115        ])
116    }
117
118    pub fn xyz() -> Self {
119        Self::from_dimensions([LasDimension::X, LasDimension::Y, LasDimension::Z])
120    }
121
122    pub fn from_dimensions<I>(dims: I) -> Self
123    where
124        I: IntoIterator<Item = LasDimension>,
125    {
126        let mut dimensions = Vec::new();
127        for dim in dims {
128            if !dimensions.contains(&dim) {
129                dimensions.push(dim);
130            }
131        }
132        Self { dimensions }
133    }
134
135    pub fn contains(&self, dim: LasDimension) -> bool {
136        self.dimensions.contains(&dim)
137    }
138
139    pub fn dimensions(&self) -> &[LasDimension] {
140        &self.dimensions
141    }
142
143    pub fn len(&self) -> usize {
144        self.dimensions.len()
145    }
146
147    pub fn is_empty(&self) -> bool {
148        self.dimensions.is_empty()
149    }
150}
151
152/// Primitive scalar types supported by LAS/COPC column data.
153#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
154pub enum ScalarType {
155    F64,
156    F32,
157    I64,
158    I32,
159    I16,
160    I8,
161    U64,
162    U32,
163    U16,
164    U8,
165    Bool,
166}
167
168/// Declares the LAS/COPC dimension and scalar type for a column.
169#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
170pub struct ColumnSpec {
171    pub dimension: LasDimension,
172    pub scalar: ScalarType,
173    /// For `LasDimension::ExtraBytes`, the fixed byte count stored for each point.
174    pub byte_width: Option<usize>,
175}
176
177impl ColumnSpec {
178    pub const fn new(dimension: LasDimension, scalar: ScalarType) -> Self {
179        Self {
180            dimension,
181            scalar,
182            byte_width: None,
183        }
184    }
185
186    pub const fn extra_bytes(byte_width: usize) -> Self {
187        Self {
188            dimension: LasDimension::ExtraBytes,
189            scalar: ScalarType::U8,
190            byte_width: Some(byte_width),
191        }
192    }
193
194    /// Returns the default fixed LAS/COPC scalar for `dimension`, when it has one.
195    pub const fn default_for(dimension: LasDimension) -> Option<Self> {
196        match dimension.default_scalar() {
197            Some(scalar) => Some(Self {
198                dimension,
199                scalar,
200                byte_width: None,
201            }),
202            None => None,
203        }
204    }
205
206    /// Returns whether this specification has the canonical scalar for its dimension.
207    pub const fn has_default_scalar(self) -> bool {
208        if matches!(self.dimension, LasDimension::ExtraBytes) {
209            matches!(self.scalar, ScalarType::U8) && self.byte_width.is_some()
210        } else {
211            self.byte_width.is_none() && self.dimension.accepts_scalar(self.scalar)
212        }
213    }
214
215    /// Returns whether `data` has the scalar type declared by this spec.
216    pub const fn matches_data(self, data: &ColumnData) -> bool {
217        self.scalar as u8 == data.scalar() as u8
218    }
219
220    /// Validate the declared scalar against the supplied data.
221    pub fn validate_data(self, data: &ColumnData) -> Result<()> {
222        if !self.matches_data(data) {
223            return Err(Error::InvalidInput(format!(
224                "column {:?} declares {:?} data but contains {:?}",
225                self.dimension,
226                self.scalar,
227                data.scalar()
228            )));
229        }
230        if self.dimension == LasDimension::ExtraBytes && self.extra_byte_width().is_none() {
231            return Err(Error::InvalidInput(
232                "ExtraBytes column requires a non-zero byte width".into(),
233            ));
234        }
235        if self.dimension != LasDimension::ExtraBytes && self.byte_width.is_some() {
236            return Err(Error::InvalidInput(format!(
237                "column {:?} cannot declare byte width {:?}",
238                self.dimension, self.byte_width
239            )));
240        }
241        Ok(())
242    }
243
244    /// Validate that this spec uses the fixed LAS/COPC scalar for its dimension.
245    pub fn validate_default_scalar(self) -> Result<()> {
246        if self.has_default_scalar() {
247            Ok(())
248        } else {
249            Err(Error::InvalidInput(format!(
250                "column {:?} declares {:?}, expected {:?}",
251                self.dimension,
252                self.scalar,
253                self.dimension.default_scalar()
254            )))
255        }
256    }
257
258    pub fn extra_byte_width(self) -> Option<usize> {
259        match (self.dimension, self.scalar, self.byte_width) {
260            (LasDimension::ExtraBytes, ScalarType::U8, Some(width)) if width > 0 => Some(width),
261            _ => None,
262        }
263    }
264
265    pub fn point_count_for_data(self, data: &ColumnData) -> Result<usize> {
266        self.validate_data(data)?;
267        if self.dimension == LasDimension::ExtraBytes {
268            let width = self.extra_byte_width().ok_or_else(|| {
269                Error::InvalidInput("ExtraBytes column requires a non-zero byte width".into())
270            })?;
271            if data.len() % width != 0 {
272                return Err(Error::InvalidInput(format!(
273                    "ExtraBytes column has {} bytes, which is not divisible by byte width {width}",
274                    data.len()
275                )));
276            }
277            Ok(data.len() / width)
278        } else {
279            Ok(data.len())
280        }
281    }
282}
283
284/// Owned column values.
285#[derive(Clone, Debug, PartialEq)]
286pub enum ColumnData {
287    F64(Vec<f64>),
288    F32(Vec<f32>),
289    I64(Vec<i64>),
290    I32(Vec<i32>),
291    I16(Vec<i16>),
292    I8(Vec<i8>),
293    U64(Vec<u64>),
294    U32(Vec<u32>),
295    U16(Vec<u16>),
296    U8(Vec<u8>),
297    Bool(Vec<bool>),
298}
299
300impl ColumnData {
301    pub fn len(&self) -> usize {
302        match self {
303            Self::F64(values) => values.len(),
304            Self::F32(values) => values.len(),
305            Self::I64(values) => values.len(),
306            Self::I32(values) => values.len(),
307            Self::I16(values) => values.len(),
308            Self::I8(values) => values.len(),
309            Self::U64(values) => values.len(),
310            Self::U32(values) => values.len(),
311            Self::U16(values) => values.len(),
312            Self::U8(values) => values.len(),
313            Self::Bool(values) => values.len(),
314        }
315    }
316
317    pub fn is_empty(&self) -> bool {
318        self.len() == 0
319    }
320
321    pub const fn scalar(&self) -> ScalarType {
322        match self {
323            Self::F64(_) => ScalarType::F64,
324            Self::F32(_) => ScalarType::F32,
325            Self::I64(_) => ScalarType::I64,
326            Self::I32(_) => ScalarType::I32,
327            Self::I16(_) => ScalarType::I16,
328            Self::I8(_) => ScalarType::I8,
329            Self::U64(_) => ScalarType::U64,
330            Self::U32(_) => ScalarType::U32,
331            Self::U16(_) => ScalarType::U16,
332            Self::U8(_) => ScalarType::U8,
333            Self::Bool(_) => ScalarType::Bool,
334        }
335    }
336
337    pub const fn scalar_type(&self) -> ScalarType {
338        self.scalar()
339    }
340
341    pub const fn matches_scalar(&self, scalar: ScalarType) -> bool {
342        self.scalar() as u8 == scalar as u8
343    }
344
345    pub fn view(&self) -> ColumnView<'_> {
346        match self {
347            Self::F64(values) => ColumnView::F64(values),
348            Self::F32(values) => ColumnView::F32(values),
349            Self::I64(values) => ColumnView::I64(values),
350            Self::I32(values) => ColumnView::I32(values),
351            Self::I16(values) => ColumnView::I16(values),
352            Self::I8(values) => ColumnView::I8(values),
353            Self::U64(values) => ColumnView::U64(values),
354            Self::U32(values) => ColumnView::U32(values),
355            Self::U16(values) => ColumnView::U16(values),
356            Self::U8(values) => ColumnView::U8(values),
357            Self::Bool(values) => ColumnView::Bool(values),
358        }
359    }
360}
361
362/// Borrowed column values.
363#[derive(Clone, Copy, Debug, PartialEq)]
364pub enum ColumnView<'a> {
365    F64(&'a [f64]),
366    F32(&'a [f32]),
367    I64(&'a [i64]),
368    I32(&'a [i32]),
369    I16(&'a [i16]),
370    I8(&'a [i8]),
371    U64(&'a [u64]),
372    U32(&'a [u32]),
373    U16(&'a [u16]),
374    U8(&'a [u8]),
375    Bool(&'a [bool]),
376}
377
378impl ColumnView<'_> {
379    pub fn len(&self) -> usize {
380        match self {
381            Self::F64(values) => values.len(),
382            Self::F32(values) => values.len(),
383            Self::I64(values) => values.len(),
384            Self::I32(values) => values.len(),
385            Self::I16(values) => values.len(),
386            Self::I8(values) => values.len(),
387            Self::U64(values) => values.len(),
388            Self::U32(values) => values.len(),
389            Self::U16(values) => values.len(),
390            Self::U8(values) => values.len(),
391            Self::Bool(values) => values.len(),
392        }
393    }
394
395    pub fn is_empty(&self) -> bool {
396        self.len() == 0
397    }
398
399    pub const fn scalar(&self) -> ScalarType {
400        match self {
401            Self::F64(_) => ScalarType::F64,
402            Self::F32(_) => ScalarType::F32,
403            Self::I64(_) => ScalarType::I64,
404            Self::I32(_) => ScalarType::I32,
405            Self::I16(_) => ScalarType::I16,
406            Self::I8(_) => ScalarType::I8,
407            Self::U64(_) => ScalarType::U64,
408            Self::U32(_) => ScalarType::U32,
409            Self::U16(_) => ScalarType::U16,
410            Self::U8(_) => ScalarType::U8,
411            Self::Bool(_) => ScalarType::Bool,
412        }
413    }
414
415    pub const fn scalar_type(&self) -> ScalarType {
416        self.scalar()
417    }
418}
419
420/// Returns the column layout available from a LAS point format.
421pub fn layout_for_las_format(format: LasPointFormat) -> Vec<ColumnSpec> {
422    let mut columns = Vec::with_capacity(27);
423
424    push_default_specs(
425        &mut columns,
426        [
427            LasDimension::X,
428            LasDimension::Y,
429            LasDimension::Z,
430            LasDimension::Intensity,
431            LasDimension::ReturnNumber,
432            LasDimension::NumberOfReturns,
433            LasDimension::Classification,
434            LasDimension::ScanDirectionFlag,
435            LasDimension::EdgeOfFlightLine,
436            LasDimension::ScanAngleRank,
437            LasDimension::UserData,
438            LasDimension::PointSourceId,
439            LasDimension::Synthetic,
440            LasDimension::KeyPoint,
441            LasDimension::Withheld,
442            LasDimension::Overlap,
443            LasDimension::ScanChannel,
444        ],
445    );
446
447    if format.has_gps_time {
448        columns.push(default_column_spec(LasDimension::GpsTime));
449    }
450    if format.has_color {
451        push_default_specs(
452            &mut columns,
453            [LasDimension::Red, LasDimension::Green, LasDimension::Blue],
454        );
455    }
456    if format.has_nir {
457        columns.push(default_column_spec(LasDimension::Nir));
458    }
459    if format.has_waveform {
460        push_default_specs(
461            &mut columns,
462            [
463                LasDimension::WaveformPacketDescriptorIndex,
464                LasDimension::WaveformPacketByteOffset,
465                LasDimension::WaveformPacketSize,
466                LasDimension::WavePacketReturnPointWaveformLocation,
467            ],
468        );
469    }
470    if format.extra_bytes > 0 {
471        columns.push(ColumnSpec::extra_bytes(usize::from(format.extra_bytes)));
472    }
473    columns
474}
475
476/// Converts scan angle degrees into the rank-style column used by existing readers.
477pub fn scan_angle_rank_from_degrees(degrees: f32) -> i16 {
478    let scaled = (degrees * 180.0 / 90.0).round() as i32;
479    scaled.clamp(i16::MIN as i32, i16::MAX as i32) as i16
480}
481
482fn push_default_specs<I>(columns: &mut Vec<ColumnSpec>, dims: I)
483where
484    I: IntoIterator<Item = LasDimension>,
485{
486    columns.extend(dims.into_iter().map(default_column_spec));
487}
488
489fn default_column_spec(dimension: LasDimension) -> ColumnSpec {
490    ColumnSpec::default_for(dimension).expect("fixed LAS dimension has a default scalar")
491}
492
493/// A column-oriented batch of LAS/COPC point values.
494#[derive(Clone, Debug, PartialEq)]
495pub struct LasColumnBatch {
496    pub len: usize,
497    pub columns: Vec<(ColumnSpec, ColumnData)>,
498}
499
500impl LasColumnBatch {
501    pub fn new(columns: Vec<(ColumnSpec, ColumnData)>) -> Result<Self> {
502        let len = match columns.first() {
503            Some((spec, data)) => spec.point_count_for_data(data)?,
504            None => 0,
505        };
506        let batch = Self { len, columns };
507        batch.validate()?;
508        Ok(batch)
509    }
510
511    pub fn len(&self) -> usize {
512        self.len
513    }
514
515    pub fn is_empty(&self) -> bool {
516        self.len == 0
517    }
518
519    pub fn column(&self, dimension: LasDimension) -> Option<&ColumnData> {
520        self.columns
521            .iter()
522            .find_map(|(spec, data)| (spec.dimension == dimension).then_some(data))
523    }
524
525    pub fn column_by_spec(&self, spec: ColumnSpec) -> Option<&ColumnData> {
526        self.columns
527            .iter()
528            .find_map(|(column_spec, data)| (*column_spec == spec).then_some(data))
529    }
530
531    pub fn column_view(&self, dimension: LasDimension) -> Option<ColumnView<'_>> {
532        self.column(dimension).map(ColumnData::view)
533    }
534
535    pub fn column_view_by_spec(&self, spec: ColumnSpec) -> Option<ColumnView<'_>> {
536        self.column_by_spec(spec).map(ColumnData::view)
537    }
538
539    /// Validate scalar declarations and column lengths for this batch.
540    pub fn validate(&self) -> Result<()> {
541        for (spec, data) in &self.columns {
542            let point_count = spec.point_count_for_data(data)?;
543            if point_count != self.len {
544                return Err(Error::InvalidInput(format!(
545                    "column {:?} has {} points but batch len is {}",
546                    spec.dimension, point_count, self.len
547                )));
548            }
549        }
550        Ok(())
551    }
552
553    /// Validate scalar declarations, fixed LAS/COPC scalar choices, and column lengths.
554    pub fn validate_default_scalars(&self) -> Result<()> {
555        self.validate()?;
556        for (spec, _) in &self.columns {
557            spec.validate_default_scalar()?;
558        }
559        Ok(())
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566
567    fn base_layout_dims() -> Vec<LasDimension> {
568        vec![
569            LasDimension::X,
570            LasDimension::Y,
571            LasDimension::Z,
572            LasDimension::Intensity,
573            LasDimension::ReturnNumber,
574            LasDimension::NumberOfReturns,
575            LasDimension::Classification,
576            LasDimension::ScanDirectionFlag,
577            LasDimension::EdgeOfFlightLine,
578            LasDimension::ScanAngleRank,
579            LasDimension::UserData,
580            LasDimension::PointSourceId,
581            LasDimension::Synthetic,
582            LasDimension::KeyPoint,
583            LasDimension::Withheld,
584            LasDimension::Overlap,
585            LasDimension::ScanChannel,
586        ]
587    }
588
589    fn assert_layout_dims(format_id: u8, expected: Vec<LasDimension>) {
590        let format = LasPointFormat::new(format_id).unwrap();
591        let layout = layout_for_las_format(format);
592        let dims: Vec<_> = layout.iter().map(|spec| spec.dimension).collect();
593        assert_eq!(expected, dims, "format {format_id}");
594        for spec in layout {
595            spec.validate_default_scalar().unwrap();
596        }
597    }
598
599    #[test]
600    fn data_reports_len_and_scalar() {
601        let data = ColumnData::U16(vec![10, 20, 30]);
602
603        assert_eq!(3, data.len());
604        assert!(!data.is_empty());
605        assert_eq!(ScalarType::U16, data.scalar());
606        assert!(data.matches_scalar(ScalarType::U16));
607    }
608
609    #[test]
610    fn batch_finds_owned_columns_and_views() {
611        let batch = LasColumnBatch::new(vec![
612            (
613                ColumnSpec::new(LasDimension::X, ScalarType::F64),
614                ColumnData::F64(vec![1.0, 2.0]),
615            ),
616            (
617                ColumnSpec::new(LasDimension::Intensity, ScalarType::U16),
618                ColumnData::U16(vec![100, 200]),
619            ),
620            (
621                ColumnSpec::new(LasDimension::Withheld, ScalarType::Bool),
622                ColumnData::Bool(vec![false, true]),
623            ),
624        ])
625        .unwrap();
626
627        assert_eq!(2, batch.len());
628        assert!(!batch.is_empty());
629        assert_eq!(
630            Some(&ColumnData::U16(vec![100, 200])),
631            batch.column(LasDimension::Intensity)
632        );
633        assert_eq!(
634            Some(ColumnView::Bool(&[false, true])),
635            batch.column_view(LasDimension::Withheld)
636        );
637    }
638
639    #[test]
640    fn batch_rejects_scalar_mismatch() {
641        let err = LasColumnBatch::new(vec![(
642            ColumnSpec::new(LasDimension::Intensity, ScalarType::U16),
643            ColumnData::U8(vec![1, 2]),
644        )])
645        .unwrap_err();
646
647        assert!(err
648            .to_string()
649            .contains("declares U16 data but contains U8"));
650    }
651
652    #[test]
653    fn batch_rejects_len_mismatch() {
654        let batch = LasColumnBatch {
655            len: 3,
656            columns: vec![(
657                ColumnSpec::new(LasDimension::X, ScalarType::F64),
658                ColumnData::F64(vec![1.0, 2.0]),
659            )],
660        };
661
662        assert!(batch.validate().is_err());
663    }
664
665    #[test]
666    fn batch_validates_fixed_width_extra_bytes() {
667        let batch = LasColumnBatch::new(vec![(
668            ColumnSpec::extra_bytes(3),
669            ColumnData::U8(vec![1, 2, 3, 4, 5, 6]),
670        )])
671        .unwrap();
672
673        assert_eq!(2, batch.len());
674        assert_eq!(
675            Some(&ColumnData::U8(vec![1, 2, 3, 4, 5, 6])),
676            batch.column(LasDimension::ExtraBytes)
677        );
678
679        let invalid = LasColumnBatch::new(vec![(
680            ColumnSpec::extra_bytes(3),
681            ColumnData::U8(vec![1, 2, 3, 4]),
682        )]);
683        assert!(invalid.is_err());
684
685        let missing_width = LasColumnBatch::new(vec![(
686            ColumnSpec::new(LasDimension::ExtraBytes, ScalarType::U8),
687            ColumnData::U8(vec![1, 2, 3]),
688        )]);
689        assert!(missing_width.is_err());
690    }
691
692    #[test]
693    fn default_scalar_validation_allows_extra_bytes() {
694        assert_eq!(
695            ColumnSpec::new(LasDimension::GpsTime, ScalarType::F64),
696            ColumnSpec::default_for(LasDimension::GpsTime).unwrap()
697        );
698        assert!(ColumnSpec::extra_bytes(4).has_default_scalar());
699        assert!(ColumnSpec::new(LasDimension::ExtraBytes, ScalarType::U8)
700            .validate_default_scalar()
701            .is_err());
702        assert!(
703            ColumnSpec::new(LasDimension::ScanAngleRank, ScalarType::F32)
704                .validate_default_scalar()
705                .is_err()
706        );
707    }
708
709    #[test]
710    fn selection_tracks_requested_dimensions() {
711        let xyz = ColumnSelection::xyz();
712        assert_eq!(
713            &[LasDimension::X, LasDimension::Y, LasDimension::Z],
714            xyz.dimensions()
715        );
716        assert!(xyz.contains(LasDimension::X));
717        assert!(!xyz.contains(LasDimension::Intensity));
718
719        let selection = ColumnSelection::from_dimensions([
720            LasDimension::Intensity,
721            LasDimension::X,
722            LasDimension::Intensity,
723        ]);
724        assert_eq!(
725            &[LasDimension::Intensity, LasDimension::X],
726            selection.dimensions()
727        );
728        assert_eq!(2, selection.len());
729        assert!(!selection.is_empty());
730
731        let all = ColumnSelection::all();
732        assert!(all.contains(LasDimension::WaveformPacketByteOffset));
733        assert!(all.contains(LasDimension::ExtraBytes));
734    }
735
736    #[test]
737    fn scan_angle_rank_uses_engine_conversion() {
738        assert_eq!(0, scan_angle_rank_from_degrees(0.0));
739        assert_eq!(91, scan_angle_rank_from_degrees(45.25));
740        assert_eq!(-91, scan_angle_rank_from_degrees(-45.25));
741        assert_eq!(i16::MAX, scan_angle_rank_from_degrees(f32::MAX));
742        assert_eq!(i16::MIN, scan_angle_rank_from_degrees(f32::MIN));
743    }
744
745    #[test]
746    fn layout_for_format_0_has_core_dimensions() {
747        assert_layout_dims(0, base_layout_dims());
748    }
749
750    #[test]
751    fn layout_for_format_3_adds_gps_and_color() {
752        let mut expected = base_layout_dims();
753        expected.extend([
754            LasDimension::GpsTime,
755            LasDimension::Red,
756            LasDimension::Green,
757            LasDimension::Blue,
758        ]);
759
760        assert_layout_dims(3, expected);
761    }
762
763    #[test]
764    fn layout_for_format_6_adds_gps() {
765        let mut expected = base_layout_dims();
766        expected.push(LasDimension::GpsTime);
767
768        assert_layout_dims(6, expected);
769    }
770
771    #[test]
772    fn layout_for_format_7_adds_gps_and_color() {
773        let mut expected = base_layout_dims();
774        expected.extend([
775            LasDimension::GpsTime,
776            LasDimension::Red,
777            LasDimension::Green,
778            LasDimension::Blue,
779        ]);
780
781        assert_layout_dims(7, expected);
782    }
783
784    #[test]
785    fn layout_for_format_8_adds_gps_color_and_nir() {
786        let mut expected = base_layout_dims();
787        expected.extend([
788            LasDimension::GpsTime,
789            LasDimension::Red,
790            LasDimension::Green,
791            LasDimension::Blue,
792            LasDimension::Nir,
793        ]);
794
795        assert_layout_dims(8, expected);
796    }
797
798    #[test]
799    fn layout_for_format_10_adds_all_optional_las_dimensions() {
800        let mut expected = base_layout_dims();
801        expected.extend([
802            LasDimension::GpsTime,
803            LasDimension::Red,
804            LasDimension::Green,
805            LasDimension::Blue,
806            LasDimension::Nir,
807            LasDimension::WaveformPacketDescriptorIndex,
808            LasDimension::WaveformPacketByteOffset,
809            LasDimension::WaveformPacketSize,
810            LasDimension::WavePacketReturnPointWaveformLocation,
811        ]);
812
813        assert_layout_dims(10, expected);
814    }
815
816    #[test]
817    fn layout_includes_extra_bytes_with_byte_width_when_format_declares_them() {
818        let mut format = LasPointFormat::new(0).unwrap();
819        format.extra_bytes = 4;
820
821        let layout = layout_for_las_format(format);
822
823        assert_eq!(Some(&ColumnSpec::extra_bytes(4)), layout.last());
824    }
825}