1use std::collections::HashSet;
4
5use crate::{Error, Result};
6
7use las::point::Format as LasPointFormat;
8
9#[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 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 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 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#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
173pub struct ColumnSpec {
174 pub dimension: LasDimension,
175 pub scalar: ScalarType,
176 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 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 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 pub const fn matches_data(self, data: &ColumnData) -> bool {
220 self.scalar as u8 == data.scalar() as u8
221 }
222
223 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 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#[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#[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
423pub 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#[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 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 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}