Skip to main content

copc_core/
columns.rs

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