1use crate::{Error, Result};
4
5use las::point::Format as LasPointFormat;
6
7#[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 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 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#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
170pub struct ColumnSpec {
171 pub dimension: LasDimension,
172 pub scalar: ScalarType,
173 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 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 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 pub const fn matches_data(self, data: &ColumnData) -> bool {
217 self.scalar as u8 == data.scalar() as u8
218 }
219
220 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 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#[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#[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
420pub 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
476pub 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#[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 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 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}