1use std::fs::File;
2use std::io::{BufReader, Read, Seek, SeekFrom};
3use std::path::Path;
4
5use copc_core::{
6 layout_for_las_format, scan_angle_rank_from_degrees, Bounds, CancelCheck, ColumnData,
7 ColumnSelection, ColumnSpec, CopcInfo, Entry, Error, LasColumnBatch, LasDimension, Result,
8};
9use las::point::Format as LasPointFormat;
10use las::{Point, Transform, Vector};
11use laz::record::{LayeredPointRecordDecompressor, RecordDecompressor};
12use laz::LazVlr;
13
14use crate::{CopcFile, LasHeader};
15
16const CANCEL_POLL_STRIDE: usize = 4_096;
17
18pub struct CopcReader<R> {
20 source: R,
21 file: CopcFile,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
26pub enum LodSelection {
27 All,
29 Resolution(f64),
31 Level(i32),
33 LevelMinMax(i32, i32),
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
39pub enum BoundsSelection {
40 All,
42 Within(Bounds),
44}
45
46#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct PointQuery {
49 pub lod: LodSelection,
50 pub bounds: BoundsSelection,
51}
52
53impl PointQuery {
54 pub const fn all() -> Self {
55 Self {
56 lod: LodSelection::All,
57 bounds: BoundsSelection::All,
58 }
59 }
60
61 pub const fn new(lod: LodSelection, bounds: BoundsSelection) -> Self {
62 Self { lod, bounds }
63 }
64}
65
66impl CopcReader<BufReader<File>> {
67 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
68 let file = File::open(path.as_ref()).map_err(|e| Error::io("open COPC file", e))?;
69 Self::open(BufReader::new(file))
70 }
71}
72
73impl<R: Read + Seek + Send> CopcReader<R> {
74 pub fn open(mut source: R) -> Result<Self> {
76 let file = CopcFile::from_reader(&mut source)?;
77 Ok(Self { source, file })
78 }
79
80 pub fn file(&self) -> &CopcFile {
81 &self.file
82 }
83
84 pub fn header(&self) -> &LasHeader {
85 self.file.header()
86 }
87
88 pub fn copc_info(&self) -> &CopcInfo {
89 self.file.copc_info()
90 }
91
92 pub fn into_inner(self) -> R {
93 self.source
94 }
95
96 pub fn points(
101 &mut self,
102 lod: LodSelection,
103 bounds: BoundsSelection,
104 ) -> Result<PointIter<'_, R>> {
105 self.points_for_query(PointQuery::new(lod, bounds))
106 }
107
108 pub fn points_for_query(&mut self, query: PointQuery) -> Result<PointIter<'_, R>> {
109 PointIter::new(&mut self.source, &self.file, query, None)
110 }
111
112 pub fn points_with_cancel<'a>(
113 &'a mut self,
114 lod: LodSelection,
115 bounds: BoundsSelection,
116 cancel: &'a dyn CancelCheck,
117 ) -> Result<PointIter<'a, R>> {
118 PointIter::new(
119 &mut self.source,
120 &self.file,
121 PointQuery::new(lod, bounds),
122 Some(cancel),
123 )
124 }
125
126 pub fn read_columns(
127 &mut self,
128 query: PointQuery,
129 selection: ColumnSelection,
130 ) -> Result<LasColumnBatch> {
131 self.read_columns_inner(query, selection, None)
132 }
133
134 pub fn read_columns_with_cancel(
135 &mut self,
136 query: PointQuery,
137 selection: ColumnSelection,
138 cancel: &dyn CancelCheck,
139 ) -> Result<LasColumnBatch> {
140 self.read_columns_inner(query, selection, Some(cancel))
141 }
142
143 fn read_columns_inner(
144 &mut self,
145 query: PointQuery,
146 selection: ColumnSelection,
147 cancel: Option<&dyn CancelCheck>,
148 ) -> Result<LasColumnBatch> {
149 let chunks = select_point_chunks(&self.file, query)?;
150 let point_format = self.file.point_format()?;
151 let transforms = self.file.transforms();
152 let bounds = match query.bounds {
153 BoundsSelection::All => None,
154 BoundsSelection::Within(bounds) => Some(bounds),
155 };
156 let mut decoder = ChunkLazDecoder::new(&mut self.source, self.file.laszip_vlr().clone())?;
157 let expected_record_size = usize::from(point_format.len());
158 if decoder.record_size() != expected_record_size {
159 return Err(Error::InvalidData(format!(
160 "LASzip item size is {} bytes, but LAS point record length is {} bytes",
161 decoder.record_size(),
162 expected_record_size
163 )));
164 }
165
166 let capacity = match bounds {
167 Some(_) => 0,
168 None => total_candidate_points(&chunks)?,
169 };
170 let mut columns = selected_column_builders(point_format, selection, capacity)?;
171 let mut point_buf = vec![0u8; expected_record_size];
172 let mut decoded_points = 0usize;
173 let mut accepted_points = 0usize;
174
175 for entry in chunks {
176 if entry.point_count <= 0 {
177 continue;
178 }
179 decoder.seek_to_chunk(entry.offset)?;
180 let points_in_chunk = usize::try_from(entry.point_count).map_err(|_| {
181 Error::InvalidData(format!(
182 "negative point count {} for {:?}",
183 entry.point_count, entry.key
184 ))
185 })?;
186 for _ in 0..points_in_chunk {
187 if decoded_points % CANCEL_POLL_STRIDE == 0 {
188 if let Some(cancel) = cancel {
189 cancel.check()?;
190 }
191 }
192
193 decoder.decompress_one(&mut point_buf)?;
194 decoded_points += 1;
195
196 let raw_point = las::raw::Point::read_from(point_buf.as_slice(), &point_format)
197 .map_err(|e| Error::Las(e.to_string()))?;
198 let x = transforms.x.direct(raw_point.x);
199 let y = transforms.y.direct(raw_point.y);
200 let z = transforms.z.direct(raw_point.z);
201 if let Some(bounds) = bounds {
202 if !bounds.contains_xyz(x, y, z) {
203 continue;
204 }
205 }
206
207 append_columns(&mut columns, &raw_point, (x, y, z))?;
208 accepted_points += 1;
209 }
210 }
211
212 let batch = LasColumnBatch {
213 len: accepted_points,
214 columns,
215 };
216 batch.validate()?;
217 Ok(batch)
218 }
219}
220
221fn selected_column_builders(
222 point_format: LasPointFormat,
223 selection: ColumnSelection,
224 capacity: usize,
225) -> Result<Vec<(ColumnSpec, ColumnData)>> {
226 layout_for_las_format(point_format)
227 .into_iter()
228 .filter(|spec| selection.contains(spec.dimension))
229 .map(|spec| empty_column(spec, capacity))
230 .collect()
231}
232
233fn empty_column(spec: ColumnSpec, capacity: usize) -> Result<(ColumnSpec, ColumnData)> {
234 let data = match spec.scalar {
235 copc_core::ScalarType::F64 => ColumnData::F64(Vec::with_capacity(capacity)),
236 copc_core::ScalarType::F32 => ColumnData::F32(Vec::with_capacity(capacity)),
237 copc_core::ScalarType::I64 => ColumnData::I64(Vec::with_capacity(capacity)),
238 copc_core::ScalarType::I32 => ColumnData::I32(Vec::with_capacity(capacity)),
239 copc_core::ScalarType::I16 => ColumnData::I16(Vec::with_capacity(capacity)),
240 copc_core::ScalarType::I8 => ColumnData::I8(Vec::with_capacity(capacity)),
241 copc_core::ScalarType::U64 => ColumnData::U64(Vec::with_capacity(capacity)),
242 copc_core::ScalarType::U32 => ColumnData::U32(Vec::with_capacity(capacity)),
243 copc_core::ScalarType::U16 => ColumnData::U16(Vec::with_capacity(capacity)),
244 copc_core::ScalarType::U8 => {
245 let capacity = if spec.dimension == LasDimension::ExtraBytes {
246 let width = spec.extra_byte_width().ok_or_else(|| {
247 Error::InvalidInput("ExtraBytes column requires a non-zero byte width".into())
248 })?;
249 capacity.checked_mul(width).ok_or_else(|| {
250 Error::InvalidInput("ExtraBytes column capacity exceeds usize range".into())
251 })?
252 } else {
253 capacity
254 };
255 ColumnData::U8(Vec::with_capacity(capacity))
256 }
257 copc_core::ScalarType::Bool => ColumnData::Bool(Vec::with_capacity(capacity)),
258 };
259 Ok((spec, data))
260}
261
262fn append_columns(
263 columns: &mut [(ColumnSpec, ColumnData)],
264 raw_point: &las::raw::Point,
265 xyz: (f64, f64, f64),
266) -> Result<()> {
267 let mut flags = raw_point.flags;
268 let is_overlap = flags.is_overlap();
269 flags.clear_overlap_class();
270 let classification = u8::from(
271 flags
272 .to_classification()
273 .map_err(|e| Error::Las(e.to_string()))?,
274 );
275 let scan_direction_flag = matches!(
276 flags.scan_direction(),
277 las::point::ScanDirection::LeftToRight
278 );
279 let scan_angle_rank = scan_angle_rank_from_degrees(f32::from(raw_point.scan_angle));
280 let context = ColumnAppendContext {
281 raw_point,
282 xyz,
283 flags,
284 classification,
285 is_overlap,
286 scan_direction_flag,
287 scan_angle_rank,
288 };
289
290 for (spec, data) in columns {
291 append_column(*spec, data, &context)?;
292 }
293 Ok(())
294}
295
296struct ColumnAppendContext<'a> {
297 raw_point: &'a las::raw::Point,
298 xyz: (f64, f64, f64),
299 flags: las::raw::point::Flags,
300 classification: u8,
301 is_overlap: bool,
302 scan_direction_flag: bool,
303 scan_angle_rank: i16,
304}
305
306fn append_column(
307 spec: ColumnSpec,
308 data: &mut ColumnData,
309 context: &ColumnAppendContext<'_>,
310) -> Result<()> {
311 let dimension = spec.dimension;
312 let scalar = data.scalar();
313 match (dimension, data) {
314 (LasDimension::X, ColumnData::F64(values)) => values.push(context.xyz.0),
315 (LasDimension::Y, ColumnData::F64(values)) => values.push(context.xyz.1),
316 (LasDimension::Z, ColumnData::F64(values)) => values.push(context.xyz.2),
317 (LasDimension::Intensity, ColumnData::U16(values)) => {
318 values.push(context.raw_point.intensity);
319 }
320 (LasDimension::ReturnNumber, ColumnData::U8(values)) => {
321 values.push(context.flags.return_number());
322 }
323 (LasDimension::NumberOfReturns, ColumnData::U8(values)) => {
324 values.push(context.flags.number_of_returns());
325 }
326 (LasDimension::Classification, ColumnData::U8(values)) => {
327 values.push(context.classification);
328 }
329 (LasDimension::ScanDirectionFlag, ColumnData::Bool(values)) => {
330 values.push(context.scan_direction_flag);
331 }
332 (LasDimension::EdgeOfFlightLine, ColumnData::Bool(values)) => {
333 values.push(context.flags.is_edge_of_flight_line());
334 }
335 (LasDimension::ScanAngleRank, ColumnData::I16(values)) => {
336 values.push(context.scan_angle_rank);
337 }
338 (LasDimension::UserData, ColumnData::U8(values)) => {
339 values.push(context.raw_point.user_data);
340 }
341 (LasDimension::PointSourceId, ColumnData::U16(values)) => {
342 values.push(context.raw_point.point_source_id);
343 }
344 (LasDimension::Synthetic, ColumnData::Bool(values)) => {
345 values.push(context.flags.is_synthetic());
346 }
347 (LasDimension::KeyPoint, ColumnData::Bool(values)) => {
348 values.push(context.flags.is_key_point());
349 }
350 (LasDimension::Withheld, ColumnData::Bool(values)) => {
351 values.push(context.flags.is_withheld());
352 }
353 (LasDimension::Overlap, ColumnData::Bool(values)) => values.push(context.is_overlap),
354 (LasDimension::ScanChannel, ColumnData::U8(values)) => {
355 values.push(context.flags.scanner_channel());
356 }
357 (LasDimension::GpsTime, ColumnData::F64(values)) => {
358 values.push(context.raw_point.gps_time.unwrap_or(0.0));
359 }
360 (LasDimension::Red, ColumnData::U16(values)) => {
361 values.push(context.raw_point.color.unwrap_or_default().red);
362 }
363 (LasDimension::Green, ColumnData::U16(values)) => {
364 values.push(context.raw_point.color.unwrap_or_default().green);
365 }
366 (LasDimension::Blue, ColumnData::U16(values)) => {
367 values.push(context.raw_point.color.unwrap_or_default().blue);
368 }
369 (LasDimension::Nir, ColumnData::U16(values)) => {
370 values.push(context.raw_point.nir.unwrap_or(0));
371 }
372 (LasDimension::WaveformPacketDescriptorIndex, ColumnData::U8(values)) => {
373 values.push(
374 context
375 .raw_point
376 .waveform
377 .unwrap_or_default()
378 .wave_packet_descriptor_index,
379 );
380 }
381 (LasDimension::WaveformPacketByteOffset, ColumnData::U64(values)) => {
382 values.push(
383 context
384 .raw_point
385 .waveform
386 .unwrap_or_default()
387 .byte_offset_to_waveform_data,
388 );
389 }
390 (LasDimension::WaveformPacketSize, ColumnData::U32(values)) => {
391 values.push(
392 context
393 .raw_point
394 .waveform
395 .unwrap_or_default()
396 .waveform_packet_size_in_bytes,
397 );
398 }
399 (LasDimension::WavePacketReturnPointWaveformLocation, ColumnData::F32(values)) => {
400 values.push(
401 context
402 .raw_point
403 .waveform
404 .unwrap_or_default()
405 .return_point_waveform_location,
406 );
407 }
408 (LasDimension::ExtraBytes, ColumnData::U8(values)) => {
409 let width = spec.extra_byte_width().ok_or_else(|| {
410 Error::InvalidData("ExtraBytes column requires a non-zero byte width".into())
411 })?;
412 if context.raw_point.extra_bytes.len() != width {
413 return Err(Error::InvalidData(format!(
414 "ExtraBytes point has {} bytes, expected {width}",
415 context.raw_point.extra_bytes.len()
416 )));
417 }
418 values.extend_from_slice(&context.raw_point.extra_bytes);
419 }
420 _ => {
421 return Err(Error::InvalidData(format!(
422 "column {:?} has incompatible data type {:?}",
423 dimension, scalar
424 )));
425 }
426 }
427 Ok(())
428}
429
430pub struct PointIter<'a, R: Read + Seek + Send> {
432 chunks: Vec<Entry>,
433 next_chunk: usize,
434 current_chunk_points_left: usize,
435 remaining_candidate_points: usize,
436 exact_size: bool,
437 point_format: LasPointFormat,
438 transforms: Vector<Transform>,
439 bounds: Option<Bounds>,
440 decoder: ChunkLazDecoder<'a, R>,
441 point_buf: Vec<u8>,
442 decoded_points: usize,
443 cancel: Option<&'a dyn CancelCheck>,
444 finished: bool,
445}
446
447impl<'a, R: Read + Seek + Send> PointIter<'a, R> {
448 fn new(
449 source: &'a mut R,
450 file: &CopcFile,
451 query: PointQuery,
452 cancel: Option<&'a dyn CancelCheck>,
453 ) -> Result<Self> {
454 let chunks = select_point_chunks(file, query)?;
455 let point_format = file.point_format()?;
456 let transforms = file.transforms();
457 let bounds = match query.bounds {
458 BoundsSelection::All => None,
459 BoundsSelection::Within(bounds) => Some(bounds),
460 };
461 let decoder = ChunkLazDecoder::new(source, file.laszip_vlr().clone())?;
462 let expected_record_size = usize::from(point_format.len());
463 if decoder.record_size() != expected_record_size {
464 return Err(Error::InvalidData(format!(
465 "LASzip item size is {} bytes, but LAS point record length is {} bytes",
466 decoder.record_size(),
467 expected_record_size
468 )));
469 }
470 let remaining_candidate_points = total_candidate_points(&chunks)?;
471 let point_buf = vec![0u8; expected_record_size];
472 Ok(Self {
473 chunks,
474 next_chunk: 0,
475 current_chunk_points_left: 0,
476 remaining_candidate_points,
477 exact_size: bounds.is_none(),
478 point_format,
479 transforms,
480 bounds,
481 decoder,
482 point_buf,
483 decoded_points: 0,
484 cancel,
485 finished: false,
486 })
487 }
488
489 fn load_next_chunk(&mut self) -> Result<bool> {
490 while self.next_chunk < self.chunks.len() {
491 let entry = self.chunks[self.next_chunk];
492 self.next_chunk += 1;
493 if entry.point_count <= 0 {
494 continue;
495 }
496 self.decoder.seek_to_chunk(entry.offset)?;
497 self.current_chunk_points_left = usize::try_from(entry.point_count).map_err(|_| {
498 Error::InvalidData(format!(
499 "negative point count {} for {:?}",
500 entry.point_count, entry.key
501 ))
502 })?;
503 return Ok(true);
504 }
505 Ok(false)
506 }
507
508 fn next_inner(&mut self) -> Result<Option<Point>> {
509 loop {
510 while self.current_chunk_points_left == 0 {
511 if !self.load_next_chunk()? {
512 return Ok(None);
513 }
514 }
515
516 if self.decoded_points % CANCEL_POLL_STRIDE == 0 {
517 if let Some(cancel) = self.cancel {
518 cancel.check()?;
519 }
520 }
521
522 self.decoder.decompress_one(&mut self.point_buf)?;
523 self.current_chunk_points_left -= 1;
524 self.remaining_candidate_points -= 1;
525 self.decoded_points += 1;
526
527 let raw_point =
528 las::raw::Point::read_from(self.point_buf.as_slice(), &self.point_format)
529 .map_err(|e| Error::Las(e.to_string()))?;
530 if let Some(bounds) = self.bounds {
531 let x = self.transforms.x.direct(raw_point.x);
532 let y = self.transforms.y.direct(raw_point.y);
533 let z = self.transforms.z.direct(raw_point.z);
534 if !bounds.contains_xyz(x, y, z) {
535 continue;
536 }
537 }
538 return Ok(Some(Point::new(raw_point, &self.transforms)));
539 }
540 }
541}
542
543impl<R: Read + Seek + Send> Iterator for PointIter<'_, R> {
544 type Item = Result<Point>;
545
546 fn next(&mut self) -> Option<Self::Item> {
547 if self.finished {
548 return None;
549 }
550 match self.next_inner() {
551 Ok(Some(point)) => Some(Ok(point)),
552 Ok(None) => {
553 self.finished = true;
554 None
555 }
556 Err(error) => {
557 self.finished = true;
558 Some(Err(error))
559 }
560 }
561 }
562
563 fn size_hint(&self) -> (usize, Option<usize>) {
564 if self.exact_size {
565 (
566 self.remaining_candidate_points,
567 Some(self.remaining_candidate_points),
568 )
569 } else {
570 (0, Some(self.remaining_candidate_points))
571 }
572 }
573}
574
575struct ChunkLazDecoder<'a, R: Read + Seek + Send> {
576 laz_vlr: LazVlr,
577 decompressor: LayeredPointRecordDecompressor<'a, &'a mut R>,
578 record_size: usize,
579}
580
581impl<'a, R: Read + Seek + Send> ChunkLazDecoder<'a, R> {
582 fn new(source: &'a mut R, laz_vlr: LazVlr) -> Result<Self> {
583 let mut decompressor = LayeredPointRecordDecompressor::new(source);
584 let record_size = configure_layered_decompressor(&mut decompressor, &laz_vlr)?;
585 Ok(Self {
586 laz_vlr,
587 decompressor,
588 record_size,
589 })
590 }
591
592 fn record_size(&self) -> usize {
593 self.record_size
594 }
595
596 fn seek_to_chunk(&mut self, offset: u64) -> Result<()> {
597 self.decompressor
598 .get_mut()
599 .seek(SeekFrom::Start(offset))
600 .map_err(|e| Error::io("seek COPC point chunk", e))?;
601 self.decompressor.reset();
602 self.record_size = configure_layered_decompressor(&mut self.decompressor, &self.laz_vlr)?;
603 Ok(())
604 }
605
606 fn decompress_one(&mut self, out: &mut [u8]) -> Result<()> {
607 self.decompressor
608 .decompress_next(out)
609 .map_err(|e| Error::io("decompress COPC point", e))
610 }
611}
612
613fn configure_layered_decompressor<R: Read + Seek>(
614 decompressor: &mut LayeredPointRecordDecompressor<'_, R>,
615 laz_vlr: &LazVlr,
616) -> Result<usize> {
617 decompressor
618 .set_fields_from(laz_vlr.items())
619 .map_err(|e| Error::Las(e.to_string()))?;
620 let record_size = decompressor.record_size();
621 if record_size == 0 {
622 return Err(Error::Unsupported(
623 "COPC point iteration requires layered LAZ point records".into(),
624 ));
625 }
626 Ok(record_size)
627}
628
629fn select_point_chunks(file: &CopcFile, query: PointQuery) -> Result<Vec<Entry>> {
630 let (level_min, level_max) = level_range(query.lod, file.copc_info())?;
631 let query_bounds = match query.bounds {
632 BoundsSelection::All => None,
633 BoundsSelection::Within(bounds) => Some(bounds),
634 };
635
636 let mut chunks = Vec::new();
637 for entry in file.hierarchy_entries() {
638 if !entry.has_point_data() {
639 continue;
640 }
641 if entry.byte_size <= 0 {
642 return Err(Error::InvalidData(format!(
643 "point chunk {:?} has invalid byte size {}",
644 entry.key, entry.byte_size
645 )));
646 }
647 if !(level_min..level_max).contains(&entry.key.level) {
648 continue;
649 }
650 if let Some(bounds) = query_bounds {
651 let node_bounds = voxel_bounds(entry.key, file.copc_info())?;
652 if !node_bounds.intersects(bounds) {
653 continue;
654 }
655 }
656 chunks.push(*entry);
657 }
658 chunks.sort_by_key(|entry| (entry.offset, entry.key));
659 Ok(chunks)
660}
661
662fn level_range(selection: LodSelection, info: &CopcInfo) -> Result<(i32, i32)> {
663 match selection {
664 LodSelection::All => Ok((0, i32::MAX)),
665 LodSelection::Resolution(resolution) => {
666 if !resolution.is_finite() || resolution <= 0.0 {
667 return Err(Error::InvalidInput(format!(
668 "resolution must be finite and positive, got {resolution}"
669 )));
670 }
671 if !info.spacing.is_finite() || info.spacing <= 0.0 {
672 return Err(Error::InvalidData(format!(
673 "COPC spacing must be finite and positive, got {}",
674 info.spacing
675 )));
676 }
677 let level_max = ((info.spacing / resolution).log2().ceil() as i64 + 1)
678 .max(1)
679 .min(i64::from(i32::MAX)) as i32;
680 Ok((0, level_max))
681 }
682 LodSelection::Level(level) => {
683 validate_level(level)?;
684 let max = level
685 .checked_add(1)
686 .ok_or_else(|| Error::InvalidInput(format!("LOD level {level} is too large")))?;
687 Ok((level, max))
688 }
689 LodSelection::LevelMinMax(min, max) => {
690 validate_level(min)?;
691 validate_level(max)?;
692 if max < min {
693 return Err(Error::InvalidInput(format!(
694 "LOD max {max} is smaller than min {min}"
695 )));
696 }
697 Ok((min, max))
698 }
699 }
700}
701
702fn validate_level(level: i32) -> Result<()> {
703 if level < 0 {
704 return Err(Error::InvalidInput(format!(
705 "LOD level must be non-negative, got {level}"
706 )));
707 }
708 Ok(())
709}
710
711fn total_candidate_points(entries: &[Entry]) -> Result<usize> {
712 entries.iter().try_fold(0usize, |total, entry| {
713 let count = usize::try_from(entry.point_count).map_err(|_| {
714 Error::InvalidData(format!(
715 "negative point count {} for {:?}",
716 entry.point_count, entry.key
717 ))
718 })?;
719 total
720 .checked_add(count)
721 .ok_or_else(|| Error::InvalidData("selected point count overflows usize".into()))
722 })
723}
724
725fn voxel_bounds(key: copc_core::VoxelKey, info: &CopcInfo) -> Result<Bounds> {
726 if key.level < 0 || key.x < 0 || key.y < 0 || key.z < 0 {
727 return Err(Error::InvalidData(format!(
728 "invalid negative voxel key {:?}",
729 key
730 )));
731 }
732 let side = (info.halfsize * 2.0) / 2.0_f64.powi(key.level);
733 let root_min = (
734 info.center.0 - info.halfsize,
735 info.center.1 - info.halfsize,
736 info.center.2 - info.halfsize,
737 );
738 let min = (
739 root_min.0 + f64::from(key.x) * side,
740 root_min.1 + f64::from(key.y) * side,
741 root_min.2 + f64::from(key.z) * side,
742 );
743 Ok(Bounds::new(min, (min.0 + side, min.1 + side, min.2 + side)))
744}
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 #[test]
751 fn selected_column_builders_include_extra_bytes_width() {
752 let mut format = LasPointFormat::new(6).unwrap();
753 format.extra_bytes = 3;
754
755 let columns = selected_column_builders(format, ColumnSelection::all(), 2).unwrap();
756
757 let extra_spec = columns
758 .iter()
759 .map(|(spec, _)| *spec)
760 .find(|spec| spec.dimension == LasDimension::ExtraBytes)
761 .expect("ExtraBytes column spec");
762 assert_eq!(Some(3), extra_spec.extra_byte_width());
763 assert_eq!(copc_core::ScalarType::U8, extra_spec.scalar);
764 }
765
766 #[test]
767 fn append_columns_preserves_fixed_width_extra_bytes() {
768 let mut format = LasPointFormat::new(6).unwrap();
769 format.extra_bytes = 3;
770 let mut columns = selected_column_builders(
771 format,
772 ColumnSelection::from_dimensions([LasDimension::X, LasDimension::ExtraBytes]),
773 1,
774 )
775 .unwrap();
776 let raw_point = las::raw::Point {
777 x: 10,
778 y: 20,
779 z: 30,
780 flags: las::raw::point::Flags::ThreeByte(1 | (1 << 4), 0, 2),
781 scan_angle: las::raw::point::ScanAngle::from(0.0),
782 extra_bytes: vec![9, 8, 7],
783 ..Default::default()
784 };
785
786 append_columns(&mut columns, &raw_point, (1.0, 2.0, 3.0)).unwrap();
787 let batch = LasColumnBatch::new(columns).unwrap();
788
789 assert_eq!(1, batch.len());
790 assert_eq!(
791 Some(&ColumnData::U8(vec![9, 8, 7])),
792 batch.column(LasDimension::ExtraBytes)
793 );
794 }
795
796 #[test]
797 fn append_columns_rejects_wrong_extra_bytes_width() {
798 let mut format = LasPointFormat::new(6).unwrap();
799 format.extra_bytes = 3;
800 let mut columns = selected_column_builders(
801 format,
802 ColumnSelection::from_dimensions([LasDimension::ExtraBytes]),
803 1,
804 )
805 .unwrap();
806 let raw_point = las::raw::Point {
807 flags: las::raw::point::Flags::ThreeByte(1 | (1 << 4), 0, 2),
808 extra_bytes: vec![9, 8],
809 ..Default::default()
810 };
811
812 let err = append_columns(&mut columns, &raw_point, (1.0, 2.0, 3.0)).unwrap_err();
813
814 assert!(err.to_string().contains("expected 3"));
815 }
816}