|
2 | 2 |
|
3 | 3 | use crate::{Error, Result}; |
4 | 4 |
|
| 5 | +use las::point::Format as LasPointFormat; |
| 6 | + |
5 | 7 | /// LAS/COPC point dimensions that can be represented as columns. |
6 | 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
7 | 9 | pub enum LasDimension { |
@@ -74,6 +76,79 @@ impl LasDimension { |
74 | 76 | } |
75 | 77 | } |
76 | 78 |
|
| 79 | +/// Requested LAS/COPC dimensions for column-oriented reads. |
| 80 | +#[derive(Clone, Debug, PartialEq, Eq)] |
| 81 | +pub struct ColumnSelection { |
| 82 | + dimensions: Vec<LasDimension>, |
| 83 | +} |
| 84 | + |
| 85 | +impl 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 | + |
77 | 152 | /// Primitive scalar types supported by LAS/COPC column data. |
78 | 153 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
79 | 154 | pub enum ScalarType { |
@@ -285,6 +360,80 @@ impl ColumnView<'_> { |
285 | 360 | } |
286 | 361 | } |
287 | 362 |
|
| 363 | +/// Returns the column layout available from a LAS point format. |
| 364 | +pub fn layout_for_las_format(format: LasPointFormat) -> Vec<ColumnSpec> { |
| 365 | + let mut columns = Vec::with_capacity(27); |
| 366 | + |
| 367 | + push_default_specs( |
| 368 | + &mut columns, |
| 369 | + [ |
| 370 | + LasDimension::X, |
| 371 | + LasDimension::Y, |
| 372 | + LasDimension::Z, |
| 373 | + LasDimension::Intensity, |
| 374 | + LasDimension::ReturnNumber, |
| 375 | + LasDimension::NumberOfReturns, |
| 376 | + LasDimension::Classification, |
| 377 | + LasDimension::ScanDirectionFlag, |
| 378 | + LasDimension::EdgeOfFlightLine, |
| 379 | + LasDimension::ScanAngleRank, |
| 380 | + LasDimension::UserData, |
| 381 | + LasDimension::PointSourceId, |
| 382 | + LasDimension::Synthetic, |
| 383 | + LasDimension::KeyPoint, |
| 384 | + LasDimension::Withheld, |
| 385 | + LasDimension::Overlap, |
| 386 | + LasDimension::ScanChannel, |
| 387 | + ], |
| 388 | + ); |
| 389 | + |
| 390 | + if format.has_gps_time { |
| 391 | + columns.push(default_column_spec(LasDimension::GpsTime)); |
| 392 | + } |
| 393 | + if format.has_color { |
| 394 | + push_default_specs( |
| 395 | + &mut columns, |
| 396 | + [LasDimension::Red, LasDimension::Green, LasDimension::Blue], |
| 397 | + ); |
| 398 | + } |
| 399 | + if format.has_nir { |
| 400 | + columns.push(default_column_spec(LasDimension::Nir)); |
| 401 | + } |
| 402 | + if format.has_waveform { |
| 403 | + push_default_specs( |
| 404 | + &mut columns, |
| 405 | + [ |
| 406 | + LasDimension::WaveformPacketDescriptorIndex, |
| 407 | + LasDimension::WaveformPacketByteOffset, |
| 408 | + LasDimension::WaveformPacketSize, |
| 409 | + LasDimension::WavePacketReturnPointWaveformLocation, |
| 410 | + ], |
| 411 | + ); |
| 412 | + } |
| 413 | + if format.extra_bytes > 0 { |
| 414 | + columns.push(ColumnSpec::new(LasDimension::ExtraBytes, ScalarType::U8)); |
| 415 | + } |
| 416 | + |
| 417 | + columns |
| 418 | +} |
| 419 | + |
| 420 | +/// Converts scan angle degrees into the rank-style column used by existing readers. |
| 421 | +pub fn scan_angle_rank_from_degrees(degrees: f32) -> i16 { |
| 422 | + let scaled = (degrees * 180.0 / 90.0).round() as i32; |
| 423 | + scaled.clamp(i16::MIN as i32, i16::MAX as i32) as i16 |
| 424 | +} |
| 425 | + |
| 426 | +fn push_default_specs<I>(columns: &mut Vec<ColumnSpec>, dims: I) |
| 427 | +where |
| 428 | + I: IntoIterator<Item = LasDimension>, |
| 429 | +{ |
| 430 | + columns.extend(dims.into_iter().map(default_column_spec)); |
| 431 | +} |
| 432 | + |
| 433 | +fn default_column_spec(dimension: LasDimension) -> ColumnSpec { |
| 434 | + ColumnSpec::default_for(dimension).expect("fixed LAS dimension has a default scalar") |
| 435 | +} |
| 436 | + |
288 | 437 | /// A column-oriented batch of LAS/COPC point values. |
289 | 438 | #[derive(Clone, Debug, PartialEq)] |
290 | 439 | pub struct LasColumnBatch { |
@@ -358,6 +507,38 @@ impl LasColumnBatch { |
358 | 507 | mod tests { |
359 | 508 | use super::*; |
360 | 509 |
|
| 510 | + fn base_layout_dims() -> Vec<LasDimension> { |
| 511 | + vec![ |
| 512 | + LasDimension::X, |
| 513 | + LasDimension::Y, |
| 514 | + LasDimension::Z, |
| 515 | + LasDimension::Intensity, |
| 516 | + LasDimension::ReturnNumber, |
| 517 | + LasDimension::NumberOfReturns, |
| 518 | + LasDimension::Classification, |
| 519 | + LasDimension::ScanDirectionFlag, |
| 520 | + LasDimension::EdgeOfFlightLine, |
| 521 | + LasDimension::ScanAngleRank, |
| 522 | + LasDimension::UserData, |
| 523 | + LasDimension::PointSourceId, |
| 524 | + LasDimension::Synthetic, |
| 525 | + LasDimension::KeyPoint, |
| 526 | + LasDimension::Withheld, |
| 527 | + LasDimension::Overlap, |
| 528 | + LasDimension::ScanChannel, |
| 529 | + ] |
| 530 | + } |
| 531 | + |
| 532 | + fn assert_layout_dims(format_id: u8, expected: Vec<LasDimension>) { |
| 533 | + let format = LasPointFormat::new(format_id).unwrap(); |
| 534 | + let layout = layout_for_las_format(format); |
| 535 | + let dims: Vec<_> = layout.iter().map(|spec| spec.dimension).collect(); |
| 536 | + assert_eq!(expected, dims, "format {format_id}"); |
| 537 | + for spec in layout { |
| 538 | + spec.validate_default_scalar().unwrap(); |
| 539 | + } |
| 540 | + } |
| 541 | + |
361 | 542 | #[test] |
362 | 543 | fn data_reports_len_and_scalar() { |
363 | 544 | let data = ColumnData::U16(vec![10, 20, 30]); |
@@ -437,4 +618,124 @@ mod tests { |
437 | 618 | .is_err() |
438 | 619 | ); |
439 | 620 | } |
| 621 | + |
| 622 | + #[test] |
| 623 | + fn selection_tracks_requested_dimensions() { |
| 624 | + let xyz = ColumnSelection::xyz(); |
| 625 | + assert_eq!( |
| 626 | + &[LasDimension::X, LasDimension::Y, LasDimension::Z], |
| 627 | + xyz.dimensions() |
| 628 | + ); |
| 629 | + assert!(xyz.contains(LasDimension::X)); |
| 630 | + assert!(!xyz.contains(LasDimension::Intensity)); |
| 631 | + |
| 632 | + let selection = ColumnSelection::from_dimensions([ |
| 633 | + LasDimension::Intensity, |
| 634 | + LasDimension::X, |
| 635 | + LasDimension::Intensity, |
| 636 | + ]); |
| 637 | + assert_eq!( |
| 638 | + &[LasDimension::Intensity, LasDimension::X], |
| 639 | + selection.dimensions() |
| 640 | + ); |
| 641 | + assert_eq!(2, selection.len()); |
| 642 | + assert!(!selection.is_empty()); |
| 643 | + |
| 644 | + let all = ColumnSelection::all(); |
| 645 | + assert!(all.contains(LasDimension::WaveformPacketByteOffset)); |
| 646 | + assert!(all.contains(LasDimension::ExtraBytes)); |
| 647 | + } |
| 648 | + |
| 649 | + #[test] |
| 650 | + fn scan_angle_rank_uses_engine_conversion() { |
| 651 | + assert_eq!(0, scan_angle_rank_from_degrees(0.0)); |
| 652 | + assert_eq!(91, scan_angle_rank_from_degrees(45.25)); |
| 653 | + assert_eq!(-91, scan_angle_rank_from_degrees(-45.25)); |
| 654 | + assert_eq!(i16::MAX, scan_angle_rank_from_degrees(f32::MAX)); |
| 655 | + assert_eq!(i16::MIN, scan_angle_rank_from_degrees(f32::MIN)); |
| 656 | + } |
| 657 | + |
| 658 | + #[test] |
| 659 | + fn layout_for_format_0_has_core_dimensions() { |
| 660 | + assert_layout_dims(0, base_layout_dims()); |
| 661 | + } |
| 662 | + |
| 663 | + #[test] |
| 664 | + fn layout_for_format_3_adds_gps_and_color() { |
| 665 | + let mut expected = base_layout_dims(); |
| 666 | + expected.extend([ |
| 667 | + LasDimension::GpsTime, |
| 668 | + LasDimension::Red, |
| 669 | + LasDimension::Green, |
| 670 | + LasDimension::Blue, |
| 671 | + ]); |
| 672 | + |
| 673 | + assert_layout_dims(3, expected); |
| 674 | + } |
| 675 | + |
| 676 | + #[test] |
| 677 | + fn layout_for_format_6_adds_gps() { |
| 678 | + let mut expected = base_layout_dims(); |
| 679 | + expected.push(LasDimension::GpsTime); |
| 680 | + |
| 681 | + assert_layout_dims(6, expected); |
| 682 | + } |
| 683 | + |
| 684 | + #[test] |
| 685 | + fn layout_for_format_7_adds_gps_and_color() { |
| 686 | + let mut expected = base_layout_dims(); |
| 687 | + expected.extend([ |
| 688 | + LasDimension::GpsTime, |
| 689 | + LasDimension::Red, |
| 690 | + LasDimension::Green, |
| 691 | + LasDimension::Blue, |
| 692 | + ]); |
| 693 | + |
| 694 | + assert_layout_dims(7, expected); |
| 695 | + } |
| 696 | + |
| 697 | + #[test] |
| 698 | + fn layout_for_format_8_adds_gps_color_and_nir() { |
| 699 | + let mut expected = base_layout_dims(); |
| 700 | + expected.extend([ |
| 701 | + LasDimension::GpsTime, |
| 702 | + LasDimension::Red, |
| 703 | + LasDimension::Green, |
| 704 | + LasDimension::Blue, |
| 705 | + LasDimension::Nir, |
| 706 | + ]); |
| 707 | + |
| 708 | + assert_layout_dims(8, expected); |
| 709 | + } |
| 710 | + |
| 711 | + #[test] |
| 712 | + fn layout_for_format_10_adds_all_optional_las_dimensions() { |
| 713 | + let mut expected = base_layout_dims(); |
| 714 | + expected.extend([ |
| 715 | + LasDimension::GpsTime, |
| 716 | + LasDimension::Red, |
| 717 | + LasDimension::Green, |
| 718 | + LasDimension::Blue, |
| 719 | + LasDimension::Nir, |
| 720 | + LasDimension::WaveformPacketDescriptorIndex, |
| 721 | + LasDimension::WaveformPacketByteOffset, |
| 722 | + LasDimension::WaveformPacketSize, |
| 723 | + LasDimension::WavePacketReturnPointWaveformLocation, |
| 724 | + ]); |
| 725 | + |
| 726 | + assert_layout_dims(10, expected); |
| 727 | + } |
| 728 | + |
| 729 | + #[test] |
| 730 | + fn layout_includes_extra_bytes_when_format_declares_them() { |
| 731 | + let mut format = LasPointFormat::new(0).unwrap(); |
| 732 | + format.extra_bytes = 4; |
| 733 | + |
| 734 | + let layout = layout_for_las_format(format); |
| 735 | + |
| 736 | + assert_eq!( |
| 737 | + Some(&ColumnSpec::new(LasDimension::ExtraBytes, ScalarType::U8)), |
| 738 | + layout.last() |
| 739 | + ); |
| 740 | + } |
440 | 741 | } |
0 commit comments