Skip to content

Commit f5ae7d6

Browse files
committed
Add column selection helpers
1 parent 585ded0 commit f5ae7d6

2 files changed

Lines changed: 305 additions & 1 deletion

File tree

copc-core/src/columns.rs

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
use crate::{Error, Result};
44

5+
use las::point::Format as LasPointFormat;
6+
57
/// LAS/COPC point dimensions that can be represented as columns.
68
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79
pub enum LasDimension {
@@ -74,6 +76,79 @@ impl LasDimension {
7476
}
7577
}
7678

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+
77152
/// Primitive scalar types supported by LAS/COPC column data.
78153
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79154
pub enum ScalarType {
@@ -285,6 +360,80 @@ impl ColumnView<'_> {
285360
}
286361
}
287362

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+
288437
/// A column-oriented batch of LAS/COPC point values.
289438
#[derive(Clone, Debug, PartialEq)]
290439
pub struct LasColumnBatch {
@@ -358,6 +507,38 @@ impl LasColumnBatch {
358507
mod tests {
359508
use super::*;
360509

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+
361542
#[test]
362543
fn data_reports_len_and_scalar() {
363544
let data = ColumnData::U16(vec![10, 20, 30]);
@@ -437,4 +618,124 @@ mod tests {
437618
.is_err()
438619
);
439620
}
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+
}
440741
}

copc-core/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ pub mod streaming;
1212

1313
pub use bounds::Bounds;
1414
pub use cancel::{CancelCheck, NeverCancel};
15-
pub use columns::{ColumnData, ColumnSpec, ColumnView, LasColumnBatch, LasDimension, ScalarType};
15+
pub use columns::{
16+
layout_for_las_format, scan_angle_rank_from_degrees, ColumnData, ColumnSelection, ColumnSpec,
17+
ColumnView, LasColumnBatch, LasDimension, ScalarType,
18+
};
1619
pub use error::{Error, Result};
1720
pub use hierarchy::{Entry, EntryAvailability, HierarchyPage, VoxelKey, HIERARCHY_ENTRY_BYTES};
1821
pub use info::CopcInfo;

0 commit comments

Comments
 (0)