1use std::fs::File;
2use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};
3use std::path::Path;
4
5use copc_core::{
6 layout_for_las_format, Bounds, CancelCheck, ColumnData, ColumnSelection, ColumnSpec, CopcInfo,
7 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;
17pub(crate) const MAX_INITIAL_COLUMN_RESERVE_POINTS: usize = 4 * 1024 * 1024;
21
22pub struct CopcReader<R> {
24 source: R,
25 file: CopcFile,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq)]
30pub enum LodSelection {
31 All,
33 Resolution(f64),
35 Level(i32),
37 LevelMinMax(i32, i32),
39}
40
41#[derive(Clone, Copy, Debug, PartialEq)]
43pub enum BoundsSelection {
44 All,
46 Within(Bounds),
48}
49
50#[derive(Clone, Copy, Debug, PartialEq)]
52pub struct PointQuery {
53 pub lod: LodSelection,
54 pub bounds: BoundsSelection,
55}
56
57impl PointQuery {
58 pub const fn all() -> Self {
59 Self {
60 lod: LodSelection::All,
61 bounds: BoundsSelection::All,
62 }
63 }
64
65 pub const fn new(lod: LodSelection, bounds: BoundsSelection) -> Self {
66 Self { lod, bounds }
67 }
68}
69
70impl CopcReader<BufReader<File>> {
71 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
72 let file = File::open(path.as_ref()).map_err(|e| Error::io("open COPC file", e))?;
73 Self::open(BufReader::new(file))
74 }
75}
76
77impl<R: Read + Seek + Send> CopcReader<R> {
78 pub fn open(mut source: R) -> Result<Self> {
80 let file = CopcFile::from_reader(&mut source)?;
81 Ok(Self { source, file })
82 }
83
84 pub fn file(&self) -> &CopcFile {
85 &self.file
86 }
87
88 pub fn header(&self) -> &LasHeader {
89 self.file.header()
90 }
91
92 pub fn copc_info(&self) -> &CopcInfo {
93 self.file.copc_info()
94 }
95
96 pub fn into_inner(self) -> R {
97 self.source
98 }
99
100 pub fn points(
105 &mut self,
106 lod: LodSelection,
107 bounds: BoundsSelection,
108 ) -> Result<PointIter<'_, R>> {
109 self.points_for_query(PointQuery::new(lod, bounds))
110 }
111
112 pub fn points_for_query(&mut self, query: PointQuery) -> Result<PointIter<'_, R>> {
113 PointIter::new(&mut self.source, &self.file, query, None)
114 }
115
116 pub fn points_with_cancel<'a>(
117 &'a mut self,
118 lod: LodSelection,
119 bounds: BoundsSelection,
120 cancel: &'a dyn CancelCheck,
121 ) -> Result<PointIter<'a, R>> {
122 PointIter::new(
123 &mut self.source,
124 &self.file,
125 PointQuery::new(lod, bounds),
126 Some(cancel),
127 )
128 }
129
130 pub fn read_columns(
131 &mut self,
132 query: PointQuery,
133 selection: ColumnSelection,
134 ) -> Result<LasColumnBatch> {
135 self.read_columns_inner(query, selection, None)
136 }
137
138 pub fn read_columns_with_cancel(
139 &mut self,
140 query: PointQuery,
141 selection: ColumnSelection,
142 cancel: &dyn CancelCheck,
143 ) -> Result<LasColumnBatch> {
144 self.read_columns_inner(query, selection, Some(cancel))
145 }
146
147 fn read_columns_inner(
148 &mut self,
149 query: PointQuery,
150 selection: ColumnSelection,
151 cancel: Option<&dyn CancelCheck>,
152 ) -> Result<LasColumnBatch> {
153 let chunks = select_point_chunks(&self.file, query)?;
154 let point_format = self.file.point_format()?;
155 let decoder = ChunkColumnDecoder::new(
156 self.file.laszip_vlr().clone(),
157 point_format,
158 self.file.transforms(),
159 match query.bounds {
160 BoundsSelection::All => None,
161 BoundsSelection::Within(bounds) => Some(bounds),
162 },
163 )?;
164
165 let capacity = match decoder.bounds {
166 Some(_) => 0,
167 None => total_candidate_points(&chunks)?.min(MAX_INITIAL_COLUMN_RESERVE_POINTS),
168 };
169 let mut columns = selected_column_builders(point_format, selection.clone(), capacity)?;
170 let mut accepted_points = 0usize;
171
172 #[cfg(not(feature = "parallel"))]
173 {
174 let mut chunk_bytes = Vec::new();
175 for entry in chunks {
176 if let Some(cancel) = cancel {
177 cancel.check()?;
178 }
179 let points_in_chunk = read_chunk_bytes(&mut self.source, entry, &mut chunk_bytes)?;
180 accepted_points +=
181 decoder.decode_into(&chunk_bytes, points_in_chunk, &mut columns, cancel)?;
182 }
183 }
184
185 #[cfg(feature = "parallel")]
186 {
187 use rayon::prelude::*;
188
189 type DecodedChunk = Result<(Vec<(ColumnSpec, ColumnData)>, usize)>;
190
191 let batch_size = rayon::current_num_threads().max(1) * 2;
192 for batch in chunks.chunks(batch_size) {
193 if let Some(cancel) = cancel {
194 cancel.check()?;
195 }
196 let mut batch_bytes = Vec::with_capacity(batch.len());
197 for entry in batch {
198 let mut chunk_bytes = Vec::new();
199 let points_in_chunk =
200 read_chunk_bytes(&mut self.source, *entry, &mut chunk_bytes)?;
201 batch_bytes.push((chunk_bytes, points_in_chunk));
202 }
203 let decoded: Vec<DecodedChunk> = batch_bytes
204 .par_iter()
205 .map(|(chunk_bytes, points_in_chunk)| {
206 let mut chunk_columns = selected_column_builders(
207 point_format,
208 selection.clone(),
209 (*points_in_chunk).min(MAX_INITIAL_COLUMN_RESERVE_POINTS),
210 )?;
211 let accepted = decoder.decode_into(
212 chunk_bytes,
213 *points_in_chunk,
214 &mut chunk_columns,
215 None,
216 )?;
217 Ok((chunk_columns, accepted))
218 })
219 .collect();
220 for result in decoded {
221 let (chunk_columns, accepted) = result?;
222 merge_columns(&mut columns, chunk_columns)?;
223 accepted_points += accepted;
224 }
225 }
226 }
227
228 let batch = LasColumnBatch {
229 len: accepted_points,
230 columns,
231 };
232 batch.validate()?;
233 Ok(batch)
234 }
235}
236
237fn read_chunk_bytes<R: Read + Seek>(
240 source: &mut R,
241 entry: Entry,
242 chunk_bytes: &mut Vec<u8>,
243) -> Result<usize> {
244 let points_in_chunk = usize::try_from(entry.point_count).map_err(|_| {
245 Error::InvalidData(format!(
246 "negative point count {} for {:?}",
247 entry.point_count, entry.key
248 ))
249 })?;
250 let byte_size = usize::try_from(entry.byte_size).map_err(|_| {
251 Error::InvalidData(format!(
252 "invalid byte size {} for {:?}",
253 entry.byte_size, entry.key
254 ))
255 })?;
256 chunk_bytes.clear();
257 chunk_bytes.resize(byte_size, 0);
258 source
259 .seek(SeekFrom::Start(entry.offset))
260 .map_err(|e| Error::io("seek COPC point chunk", e))?;
261 source
262 .read_exact(chunk_bytes)
263 .map_err(|e| Error::io("read COPC point chunk", e))?;
264 Ok(points_in_chunk)
265}
266
267pub(crate) struct ChunkColumnDecoder {
269 laz_vlr: LazVlr,
270 layout: ExtendedPointLayout,
271 transforms: Vector<Transform>,
272 bounds: Option<Bounds>,
273 record_size: usize,
274}
275
276impl ChunkColumnDecoder {
277 pub(crate) fn new(
278 laz_vlr: LazVlr,
279 point_format: LasPointFormat,
280 transforms: Vector<Transform>,
281 bounds: Option<Bounds>,
282 ) -> Result<Self> {
283 let layout = ExtendedPointLayout::for_format(&point_format)?;
284 let record_size = validated_record_size(&laz_vlr, &point_format)?;
285 Ok(Self {
286 laz_vlr,
287 layout,
288 transforms,
289 bounds,
290 record_size,
291 })
292 }
293
294 pub(crate) fn decode_into(
298 &self,
299 chunk_bytes: &[u8],
300 points_in_chunk: usize,
301 columns: &mut [(ColumnSpec, ColumnData)],
302 cancel: Option<&dyn CancelCheck>,
303 ) -> Result<usize> {
304 let mut decompressor = LayeredPointRecordDecompressor::new(Cursor::new(chunk_bytes));
305 configure_layered_decompressor(&mut decompressor, &self.laz_vlr)?;
306 let mut point_buf = vec![0u8; self.record_size];
307 let mut accepted = 0usize;
308 for decoded in 0..points_in_chunk {
309 if decoded.is_multiple_of(CANCEL_POLL_STRIDE) {
310 if let Some(cancel) = cancel {
311 cancel.check()?;
312 }
313 }
314 decompressor
315 .decompress_next(&mut point_buf)
316 .map_err(|e| Error::io("decompress COPC point", e))?;
317
318 let x = self.transforms.x.direct(i32_at(&point_buf, 0));
319 let y = self.transforms.y.direct(i32_at(&point_buf, 4));
320 let z = self.transforms.z.direct(i32_at(&point_buf, 8));
321 if let Some(bounds) = self.bounds {
322 if !bounds.contains_xyz(x, y, z) {
323 continue;
324 }
325 }
326
327 append_columns(columns, &point_buf, &self.layout, (x, y, z))?;
328 accepted += 1;
329 }
330 Ok(accepted)
331 }
332}
333
334pub(crate) fn decode_chunk_points(
337 chunk_bytes: &[u8],
338 points_in_chunk: usize,
339 laz_vlr: &LazVlr,
340 point_format: &LasPointFormat,
341 transforms: &Vector<Transform>,
342 bounds: Option<Bounds>,
343 out: &mut Vec<Point>,
344) -> Result<()> {
345 let mut decompressor = LayeredPointRecordDecompressor::new(Cursor::new(chunk_bytes));
346 let record_size = configure_layered_decompressor(&mut decompressor, laz_vlr)?;
347 if record_size != usize::from(point_format.len()) {
348 return Err(Error::InvalidData(format!(
349 "LASzip item size is {record_size} bytes, but LAS point record length is {} bytes",
350 point_format.len()
351 )));
352 }
353 let mut point_buf = vec![0u8; record_size];
354 for _ in 0..points_in_chunk {
355 decompressor
356 .decompress_next(&mut point_buf)
357 .map_err(|e| Error::io("decompress COPC point", e))?;
358 let raw_point = las::raw::Point::read_from(point_buf.as_slice(), point_format)
359 .map_err(|e| Error::Las(e.to_string()))?;
360 if let Some(bounds) = bounds {
361 let x = transforms.x.direct(raw_point.x);
362 let y = transforms.y.direct(raw_point.y);
363 let z = transforms.z.direct(raw_point.z);
364 if !bounds.contains_xyz(x, y, z) {
365 continue;
366 }
367 }
368 out.push(Point::new(raw_point, transforms));
369 }
370 Ok(())
371}
372
373fn validated_record_size(laz_vlr: &LazVlr, point_format: &LasPointFormat) -> Result<usize> {
376 let mut probe = LayeredPointRecordDecompressor::new(Cursor::new(&[][..]));
377 let record_size = configure_layered_decompressor(&mut probe, laz_vlr)?;
378 let expected = usize::from(point_format.len());
379 if record_size != expected {
380 return Err(Error::InvalidData(format!(
381 "LASzip item size is {record_size} bytes, but LAS point record length is {expected} bytes"
382 )));
383 }
384 Ok(expected)
385}
386
387#[cfg(feature = "parallel")]
389fn merge_columns(
390 dst: &mut [(ColumnSpec, ColumnData)],
391 src: Vec<(ColumnSpec, ColumnData)>,
392) -> Result<()> {
393 if dst.len() != src.len() {
394 return Err(Error::InvalidData(format!(
395 "chunk produced {} columns, expected {}",
396 src.len(),
397 dst.len()
398 )));
399 }
400 for ((dst_spec, dst_data), (src_spec, src_data)) in dst.iter_mut().zip(src) {
401 if *dst_spec != src_spec {
402 return Err(Error::InvalidData(format!(
403 "chunk column spec mismatch: found {src_spec:?}, expected {dst_spec:?}"
404 )));
405 }
406 match (dst_data, src_data) {
407 (ColumnData::F64(dst), ColumnData::F64(src)) => dst.extend_from_slice(&src),
408 (ColumnData::F32(dst), ColumnData::F32(src)) => dst.extend_from_slice(&src),
409 (ColumnData::I64(dst), ColumnData::I64(src)) => dst.extend_from_slice(&src),
410 (ColumnData::I32(dst), ColumnData::I32(src)) => dst.extend_from_slice(&src),
411 (ColumnData::I16(dst), ColumnData::I16(src)) => dst.extend_from_slice(&src),
412 (ColumnData::I8(dst), ColumnData::I8(src)) => dst.extend_from_slice(&src),
413 (ColumnData::U64(dst), ColumnData::U64(src)) => dst.extend_from_slice(&src),
414 (ColumnData::U32(dst), ColumnData::U32(src)) => dst.extend_from_slice(&src),
415 (ColumnData::U16(dst), ColumnData::U16(src)) => dst.extend_from_slice(&src),
416 (ColumnData::U8(dst), ColumnData::U8(src)) => dst.extend_from_slice(&src),
417 (ColumnData::Bool(dst), ColumnData::Bool(src)) => dst.extend_from_slice(&src),
418 _ => {
419 return Err(Error::InvalidData(
420 "chunk column scalar mismatch during merge".into(),
421 ))
422 }
423 }
424 }
425 Ok(())
426}
427
428pub(crate) fn selected_column_builders(
429 point_format: LasPointFormat,
430 selection: ColumnSelection,
431 capacity: usize,
432) -> Result<Vec<(ColumnSpec, ColumnData)>> {
433 layout_for_las_format(point_format)
434 .into_iter()
435 .filter(|spec| selection.contains(spec.dimension))
436 .map(|spec| empty_column(spec, capacity))
437 .collect()
438}
439
440fn empty_column(spec: ColumnSpec, capacity: usize) -> Result<(ColumnSpec, ColumnData)> {
441 let data = match spec.scalar {
442 copc_core::ScalarType::F64 => ColumnData::F64(Vec::with_capacity(capacity)),
443 copc_core::ScalarType::F32 => ColumnData::F32(Vec::with_capacity(capacity)),
444 copc_core::ScalarType::I64 => ColumnData::I64(Vec::with_capacity(capacity)),
445 copc_core::ScalarType::I32 => ColumnData::I32(Vec::with_capacity(capacity)),
446 copc_core::ScalarType::I16 => ColumnData::I16(Vec::with_capacity(capacity)),
447 copc_core::ScalarType::I8 => ColumnData::I8(Vec::with_capacity(capacity)),
448 copc_core::ScalarType::U64 => ColumnData::U64(Vec::with_capacity(capacity)),
449 copc_core::ScalarType::U32 => ColumnData::U32(Vec::with_capacity(capacity)),
450 copc_core::ScalarType::U16 => ColumnData::U16(Vec::with_capacity(capacity)),
451 copc_core::ScalarType::U8 => {
452 let capacity = if spec.dimension == LasDimension::ExtraBytes {
453 let width = spec.extra_byte_width().ok_or_else(|| {
454 Error::InvalidInput("ExtraBytes column requires a non-zero byte width".into())
455 })?;
456 capacity.checked_mul(width).ok_or_else(|| {
457 Error::InvalidInput("ExtraBytes column capacity exceeds usize range".into())
458 })?
459 } else {
460 capacity
461 };
462 ColumnData::U8(Vec::with_capacity(capacity))
463 }
464 copc_core::ScalarType::Bool => ColumnData::Bool(Vec::with_capacity(capacity)),
465 };
466 Ok((spec, data))
467}
468
469const LAS_14_SCAN_ANGLE_SCALE: f32 = 0.006;
472const OVERLAP_CLASSIFICATION_CODE: u8 = 12;
475
476struct ExtendedPointLayout {
480 color_offset: Option<usize>,
481 nir_offset: Option<usize>,
482 waveform_offset: Option<usize>,
483 extra_offset: usize,
484 extra_len: usize,
485}
486
487impl ExtendedPointLayout {
488 fn for_format(format: &LasPointFormat) -> Result<Self> {
489 if !format.is_extended {
490 return Err(Error::Unsupported(
491 "COPC column reads require extended point formats (6-10)".into(),
492 ));
493 }
494 let mut cursor = 30usize;
495 let color_offset = if format.has_color {
496 let offset = cursor;
497 cursor += 6;
498 Some(offset)
499 } else {
500 None
501 };
502 let nir_offset = if format.has_nir {
503 let offset = cursor;
504 cursor += 2;
505 Some(offset)
506 } else {
507 None
508 };
509 let waveform_offset = if format.has_waveform {
510 let offset = cursor;
511 cursor += 29;
512 Some(offset)
513 } else {
514 None
515 };
516 let extra_offset = cursor;
517 let extra_len = usize::from(format.extra_bytes);
518 debug_assert_eq!(extra_offset + extra_len, usize::from(format.len()));
519 Ok(Self {
520 color_offset,
521 nir_offset,
522 waveform_offset,
523 extra_offset,
524 extra_len,
525 })
526 }
527}
528
529fn append_columns(
530 columns: &mut [(ColumnSpec, ColumnData)],
531 buf: &[u8],
532 layout: &ExtendedPointLayout,
533 xyz: (f64, f64, f64),
534) -> Result<()> {
535 let raw_class = buf[16];
536 let context = ColumnAppendContext {
537 buf,
538 layout,
539 xyz,
540 classification: if raw_class == OVERLAP_CLASSIFICATION_CODE {
541 1
542 } else {
543 raw_class
544 },
545 is_overlap: buf[15] & 8 == 8,
546 };
547
548 for (spec, data) in columns {
549 append_column(*spec, data, &context)?;
550 }
551 Ok(())
552}
553
554struct ColumnAppendContext<'a> {
555 buf: &'a [u8],
556 layout: &'a ExtendedPointLayout,
557 xyz: (f64, f64, f64),
558 classification: u8,
559 is_overlap: bool,
560}
561
562#[inline]
563fn i32_at(buf: &[u8], offset: usize) -> i32 {
564 i32::from_le_bytes(buf[offset..offset + 4].try_into().expect("i32 width"))
565}
566
567#[inline]
568fn u16_at(buf: &[u8], offset: usize) -> u16 {
569 u16::from_le_bytes(buf[offset..offset + 2].try_into().expect("u16 width"))
570}
571
572#[inline]
573fn u32_at(buf: &[u8], offset: usize) -> u32 {
574 u32::from_le_bytes(buf[offset..offset + 4].try_into().expect("u32 width"))
575}
576
577#[inline]
578fn u64_at(buf: &[u8], offset: usize) -> u64 {
579 u64::from_le_bytes(buf[offset..offset + 8].try_into().expect("u64 width"))
580}
581
582#[inline]
583fn f32_at(buf: &[u8], offset: usize) -> f32 {
584 f32::from_le_bytes(buf[offset..offset + 4].try_into().expect("f32 width"))
585}
586
587#[inline]
588fn f64_at(buf: &[u8], offset: usize) -> f64 {
589 f64::from_le_bytes(buf[offset..offset + 8].try_into().expect("f64 width"))
590}
591
592#[inline]
593fn i16_at(buf: &[u8], offset: usize) -> i16 {
594 i16::from_le_bytes(buf[offset..offset + 2].try_into().expect("i16 width"))
595}
596
597fn append_column(
598 spec: ColumnSpec,
599 data: &mut ColumnData,
600 context: &ColumnAppendContext<'_>,
601) -> Result<()> {
602 let dimension = spec.dimension;
603 let scalar = data.scalar();
604 let buf = context.buf;
605 let layout = context.layout;
606 match (dimension, data) {
607 (LasDimension::X, ColumnData::F64(values)) => values.push(context.xyz.0),
608 (LasDimension::Y, ColumnData::F64(values)) => values.push(context.xyz.1),
609 (LasDimension::Z, ColumnData::F64(values)) => values.push(context.xyz.2),
610 (LasDimension::Intensity, ColumnData::U16(values)) => {
611 values.push(u16_at(buf, 12));
612 }
613 (LasDimension::ReturnNumber, ColumnData::U8(values)) => {
614 values.push(buf[14] & 15);
615 }
616 (LasDimension::NumberOfReturns, ColumnData::U8(values)) => {
617 values.push((buf[14] >> 4) & 15);
618 }
619 (LasDimension::Classification, ColumnData::U8(values)) => {
620 values.push(context.classification);
621 }
622 (LasDimension::ScanDirectionFlag, ColumnData::Bool(values)) => {
623 values.push((buf[15] >> 6) & 1 == 1);
624 }
625 (LasDimension::EdgeOfFlightLine, ColumnData::Bool(values)) => {
626 values.push((buf[15] >> 7) == 1);
627 }
628 (LasDimension::ScanAngle, ColumnData::F32(values)) => {
629 values.push(f32::from(i16_at(buf, 18)) * LAS_14_SCAN_ANGLE_SCALE);
630 }
631 (LasDimension::UserData, ColumnData::U8(values)) => {
632 values.push(buf[17]);
633 }
634 (LasDimension::PointSourceId, ColumnData::U16(values)) => {
635 values.push(u16_at(buf, 20));
636 }
637 (LasDimension::Synthetic, ColumnData::Bool(values)) => {
638 values.push(buf[15] & 1 == 1);
639 }
640 (LasDimension::KeyPoint, ColumnData::Bool(values)) => {
641 values.push(buf[15] & 2 == 2);
642 }
643 (LasDimension::Withheld, ColumnData::Bool(values)) => {
644 values.push(buf[15] & 4 == 4);
645 }
646 (LasDimension::Overlap, ColumnData::Bool(values)) => values.push(context.is_overlap),
647 (LasDimension::ScanChannel, ColumnData::U8(values)) => {
648 values.push((buf[15] >> 4) & 3);
649 }
650 (LasDimension::GpsTime, ColumnData::F64(values)) => {
651 values.push(f64_at(buf, 22));
652 }
653 (LasDimension::Red, ColumnData::U16(values)) => {
654 values.push(layout.color_offset.map_or(0, |o| u16_at(buf, o)));
655 }
656 (LasDimension::Green, ColumnData::U16(values)) => {
657 values.push(layout.color_offset.map_or(0, |o| u16_at(buf, o + 2)));
658 }
659 (LasDimension::Blue, ColumnData::U16(values)) => {
660 values.push(layout.color_offset.map_or(0, |o| u16_at(buf, o + 4)));
661 }
662 (LasDimension::Nir, ColumnData::U16(values)) => {
663 values.push(layout.nir_offset.map_or(0, |o| u16_at(buf, o)));
664 }
665 (LasDimension::WaveformPacketDescriptorIndex, ColumnData::U8(values)) => {
666 values.push(layout.waveform_offset.map_or(0, |o| buf[o]));
667 }
668 (LasDimension::WaveformPacketByteOffset, ColumnData::U64(values)) => {
669 values.push(layout.waveform_offset.map_or(0, |o| u64_at(buf, o + 1)));
670 }
671 (LasDimension::WaveformPacketSize, ColumnData::U32(values)) => {
672 values.push(layout.waveform_offset.map_or(0, |o| u32_at(buf, o + 9)));
673 }
674 (LasDimension::WavePacketReturnPointWaveformLocation, ColumnData::F32(values)) => {
675 values.push(layout.waveform_offset.map_or(0.0, |o| f32_at(buf, o + 13)));
676 }
677 (LasDimension::ExtraBytes, ColumnData::U8(values)) => {
678 let width = spec.extra_byte_width().ok_or_else(|| {
679 Error::InvalidData("ExtraBytes column requires a non-zero byte width".into())
680 })?;
681 if layout.extra_len != width {
682 return Err(Error::InvalidData(format!(
683 "ExtraBytes point has {} bytes, expected {width}",
684 layout.extra_len
685 )));
686 }
687 values.extend_from_slice(&buf[layout.extra_offset..layout.extra_offset + width]);
688 }
689 _ => {
690 return Err(Error::InvalidData(format!(
691 "column {:?} has incompatible data type {:?}",
692 dimension, scalar
693 )));
694 }
695 }
696 Ok(())
697}
698
699pub struct PointIter<'a, R: Read + Seek + Send> {
701 chunks: Vec<Entry>,
702 next_chunk: usize,
703 current_chunk_points_left: usize,
704 remaining_candidate_points: usize,
705 exact_size: bool,
706 point_format: LasPointFormat,
707 transforms: Vector<Transform>,
708 bounds: Option<Bounds>,
709 decoder: ChunkLazDecoder<'a, R>,
710 point_buf: Vec<u8>,
711 decoded_points: usize,
712 cancel: Option<&'a dyn CancelCheck>,
713 finished: bool,
714}
715
716impl<'a, R: Read + Seek + Send> PointIter<'a, R> {
717 fn new(
718 source: &'a mut R,
719 file: &CopcFile,
720 query: PointQuery,
721 cancel: Option<&'a dyn CancelCheck>,
722 ) -> Result<Self> {
723 let QuerySetup {
724 chunks,
725 point_format,
726 transforms,
727 bounds,
728 decoder,
729 record_size,
730 } = QuerySetup::new(source, file, query)?;
731 let remaining_candidate_points = total_candidate_points(&chunks)?;
732 let point_buf = vec![0u8; record_size];
733 Ok(Self {
734 chunks,
735 next_chunk: 0,
736 current_chunk_points_left: 0,
737 remaining_candidate_points,
738 exact_size: bounds.is_none(),
739 point_format,
740 transforms,
741 bounds,
742 decoder,
743 point_buf,
744 decoded_points: 0,
745 cancel,
746 finished: false,
747 })
748 }
749
750 fn load_next_chunk(&mut self) -> Result<bool> {
751 while self.next_chunk < self.chunks.len() {
752 let entry = self.chunks[self.next_chunk];
753 self.next_chunk += 1;
754 if entry.point_count <= 0 {
755 continue;
756 }
757 let byte_size = u64::try_from(entry.byte_size).map_err(|_| {
758 Error::InvalidData(format!(
759 "negative byte size {} for {:?}",
760 entry.byte_size, entry.key
761 ))
762 })?;
763 self.decoder.seek_to_chunk(entry.offset, byte_size)?;
764 self.current_chunk_points_left = usize::try_from(entry.point_count).map_err(|_| {
765 Error::InvalidData(format!(
766 "negative point count {} for {:?}",
767 entry.point_count, entry.key
768 ))
769 })?;
770 return Ok(true);
771 }
772 Ok(false)
773 }
774
775 fn next_inner(&mut self) -> Result<Option<Point>> {
776 loop {
777 while self.current_chunk_points_left == 0 {
778 if !self.load_next_chunk()? {
779 return Ok(None);
780 }
781 }
782
783 if self.decoded_points.is_multiple_of(CANCEL_POLL_STRIDE) {
784 if let Some(cancel) = self.cancel {
785 cancel.check()?;
786 }
787 }
788
789 self.decoder.decompress_one(&mut self.point_buf)?;
790 self.current_chunk_points_left -= 1;
791 self.remaining_candidate_points -= 1;
792 self.decoded_points += 1;
793
794 let raw_point =
795 las::raw::Point::read_from(self.point_buf.as_slice(), &self.point_format)
796 .map_err(|e| Error::Las(e.to_string()))?;
797 if let Some(bounds) = self.bounds {
798 let x = self.transforms.x.direct(raw_point.x);
799 let y = self.transforms.y.direct(raw_point.y);
800 let z = self.transforms.z.direct(raw_point.z);
801 if !bounds.contains_xyz(x, y, z) {
802 continue;
803 }
804 }
805 return Ok(Some(Point::new(raw_point, &self.transforms)));
806 }
807 }
808}
809
810impl<R: Read + Seek + Send> Iterator for PointIter<'_, R> {
811 type Item = Result<Point>;
812
813 fn next(&mut self) -> Option<Self::Item> {
814 if self.finished {
815 return None;
816 }
817 match self.next_inner() {
818 Ok(Some(point)) => Some(Ok(point)),
819 Ok(None) => {
820 self.finished = true;
821 None
822 }
823 Err(error) => {
824 self.finished = true;
825 Some(Err(error))
826 }
827 }
828 }
829
830 fn size_hint(&self) -> (usize, Option<usize>) {
831 if self.exact_size {
832 (
833 self.remaining_candidate_points,
834 Some(self.remaining_candidate_points),
835 )
836 } else {
837 (0, Some(self.remaining_candidate_points))
838 }
839 }
840}
841
842struct QuerySetup<'a, R: Read + Seek + Send> {
845 chunks: Vec<Entry>,
846 point_format: LasPointFormat,
847 transforms: Vector<Transform>,
848 bounds: Option<Bounds>,
849 decoder: ChunkLazDecoder<'a, R>,
850 record_size: usize,
851}
852
853impl<'a, R: Read + Seek + Send> QuerySetup<'a, R> {
854 fn new(source: &'a mut R, file: &CopcFile, query: PointQuery) -> Result<Self> {
855 let chunks = select_point_chunks(file, query)?;
856 let point_format = file.point_format()?;
857 let transforms = file.transforms();
858 let bounds = match query.bounds {
859 BoundsSelection::All => None,
860 BoundsSelection::Within(bounds) => Some(bounds),
861 };
862 let decoder = ChunkLazDecoder::new(source, file.laszip_vlr().clone())?;
863 let record_size = usize::from(point_format.len());
864 if decoder.record_size() != record_size {
865 return Err(Error::InvalidData(format!(
866 "LASzip item size is {} bytes, but LAS point record length is {} bytes",
867 decoder.record_size(),
868 record_size
869 )));
870 }
871 Ok(Self {
872 chunks,
873 point_format,
874 transforms,
875 bounds,
876 decoder,
877 record_size,
878 })
879 }
880}
881
882struct ChunkLazDecoder<'a, R: Read + Seek + Send> {
883 laz_vlr: LazVlr,
884 decompressor: LayeredPointRecordDecompressor<'a, ChunkReadWindow<'a, R>>,
885 record_size: usize,
886}
887
888impl<'a, R: Read + Seek + Send> ChunkLazDecoder<'a, R> {
889 fn new(source: &'a mut R, laz_vlr: LazVlr) -> Result<Self> {
890 let mut decompressor = LayeredPointRecordDecompressor::new(ChunkReadWindow::new(source));
891 let record_size = configure_layered_decompressor(&mut decompressor, &laz_vlr)?;
892 Ok(Self {
893 laz_vlr,
894 decompressor,
895 record_size,
896 })
897 }
898
899 fn record_size(&self) -> usize {
900 self.record_size
901 }
902
903 fn seek_to_chunk(&mut self, offset: u64, byte_size: u64) -> Result<()> {
904 self.decompressor.get_mut().set_range(offset, byte_size)?;
905 self.decompressor.reset();
906 self.record_size = configure_layered_decompressor(&mut self.decompressor, &self.laz_vlr)?;
907 Ok(())
908 }
909
910 fn decompress_one(&mut self, out: &mut [u8]) -> Result<()> {
911 self.decompressor
912 .decompress_next(out)
913 .map_err(|e| Error::io("decompress COPC point", e))
914 }
915}
916
917struct ChunkReadWindow<'a, R> {
921 source: &'a mut R,
922 start: u64,
923 end: u64,
924 position: u64,
925}
926
927impl<'a, R: Read + Seek> ChunkReadWindow<'a, R> {
928 fn new(source: &'a mut R) -> Self {
929 Self {
930 source,
931 start: 0,
932 end: 0,
933 position: 0,
934 }
935 }
936
937 fn set_range(&mut self, start: u64, byte_size: u64) -> Result<()> {
938 let end = start
939 .checked_add(byte_size)
940 .ok_or_else(|| Error::InvalidData("COPC point chunk range overflows u64".into()))?;
941 self.source
942 .seek(SeekFrom::Start(start))
943 .map_err(|e| Error::io("seek COPC point chunk", e))?;
944 self.start = start;
945 self.end = end;
946 self.position = start;
947 Ok(())
948 }
949}
950
951impl<R: Read + Seek> Read for ChunkReadWindow<'_, R> {
952 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
953 let remaining = self.end.saturating_sub(self.position);
954 if remaining == 0 {
955 return Ok(0);
956 }
957 let allowed = usize::try_from(remaining)
958 .unwrap_or(usize::MAX)
959 .min(buf.len());
960 let read = self.source.read(&mut buf[..allowed])?;
961 self.position = self
962 .position
963 .checked_add(read as u64)
964 .ok_or_else(|| std::io::Error::other("chunk read position overflow"))?;
965 Ok(read)
966 }
967}
968
969impl<R: Read + Seek> Seek for ChunkReadWindow<'_, R> {
970 fn seek(&mut self, position: SeekFrom) -> std::io::Result<u64> {
971 let target = match position {
972 SeekFrom::Start(offset) => Some(offset),
973 SeekFrom::Current(delta) => self.position.checked_add_signed(delta),
974 SeekFrom::End(delta) => self.end.checked_add_signed(delta),
975 }
976 .filter(|target| (self.start..=self.end).contains(target))
977 .ok_or_else(|| {
978 std::io::Error::new(
979 std::io::ErrorKind::InvalidInput,
980 "seek outside COPC point chunk",
981 )
982 })?;
983 self.source.seek(SeekFrom::Start(target))?;
984 self.position = target;
985 Ok(target)
986 }
987}
988
989fn configure_layered_decompressor<R: Read + Seek>(
990 decompressor: &mut LayeredPointRecordDecompressor<'_, R>,
991 laz_vlr: &LazVlr,
992) -> Result<usize> {
993 decompressor
994 .set_fields_from(laz_vlr.items())
995 .map_err(|e| Error::Las(e.to_string()))?;
996 let record_size = decompressor.record_size();
997 if record_size == 0 {
998 return Err(Error::Unsupported(
999 "COPC point iteration requires layered LAZ point records".into(),
1000 ));
1001 }
1002 Ok(record_size)
1003}
1004
1005fn select_point_chunks(file: &CopcFile, query: PointQuery) -> Result<Vec<Entry>> {
1006 select_point_chunks_from(file.hierarchy_entries(), file.copc_info(), query)
1007}
1008
1009pub(crate) fn select_point_chunks_from<'a, I>(
1010 entries: I,
1011 info: &CopcInfo,
1012 query: PointQuery,
1013) -> Result<Vec<Entry>>
1014where
1015 I: IntoIterator<Item = &'a Entry>,
1016{
1017 validate_query(query)?;
1018 let (level_min, level_max) = level_range(query.lod, info)?;
1019 let query_bounds = match query.bounds {
1020 BoundsSelection::All => None,
1021 BoundsSelection::Within(bounds) => Some(bounds),
1022 };
1023
1024 let mut chunks = Vec::new();
1025 for entry in entries {
1026 if !entry.has_point_data() {
1027 continue;
1028 }
1029 if entry.byte_size <= 0 {
1030 return Err(Error::InvalidData(format!(
1031 "point chunk {:?} has invalid byte size {}",
1032 entry.key, entry.byte_size
1033 )));
1034 }
1035 if !(level_min..level_max).contains(&entry.key.level) {
1036 continue;
1037 }
1038 if let Some(bounds) = query_bounds {
1039 let node_bounds = voxel_bounds(entry.key, info)?;
1040 if !node_bounds.intersects(bounds) {
1041 continue;
1042 }
1043 }
1044 chunks.push(*entry);
1045 }
1046 chunks.sort_by_key(|entry| (entry.offset, entry.key));
1047 Ok(chunks)
1048}
1049
1050pub(crate) fn validate_query(query: PointQuery) -> Result<()> {
1051 if let BoundsSelection::Within(bounds) = query.bounds {
1052 for (name, value) in [
1053 ("min x", bounds.min.0),
1054 ("min y", bounds.min.1),
1055 ("min z", bounds.min.2),
1056 ("max x", bounds.max.0),
1057 ("max y", bounds.max.1),
1058 ("max z", bounds.max.2),
1059 ] {
1060 if !value.is_finite() {
1061 return Err(Error::InvalidInput(format!(
1062 "query bounds {name} must be finite, got {value}"
1063 )));
1064 }
1065 }
1066 for (axis, min, max) in [
1067 ("x", bounds.min.0, bounds.max.0),
1068 ("y", bounds.min.1, bounds.max.1),
1069 ("z", bounds.min.2, bounds.max.2),
1070 ] {
1071 if min > max {
1072 return Err(Error::InvalidInput(format!(
1073 "query bounds {axis} minimum {min} exceeds maximum {max}"
1074 )));
1075 }
1076 }
1077 }
1078 Ok(())
1079}
1080
1081pub(crate) fn level_range(selection: LodSelection, info: &CopcInfo) -> Result<(i32, i32)> {
1082 match selection {
1083 LodSelection::All => Ok((0, i32::MAX)),
1084 LodSelection::Resolution(resolution) => {
1085 if !resolution.is_finite() || resolution <= 0.0 {
1086 return Err(Error::InvalidInput(format!(
1087 "resolution must be finite and positive, got {resolution}"
1088 )));
1089 }
1090 if !info.spacing.is_finite() || info.spacing <= 0.0 {
1091 return Err(Error::InvalidData(format!(
1092 "COPC spacing must be finite and positive, got {}",
1093 info.spacing
1094 )));
1095 }
1096 let level_max = ((info.spacing / resolution).log2().ceil() as i64 + 1)
1097 .max(1)
1098 .min(i64::from(i32::MAX)) as i32;
1099 Ok((0, level_max))
1100 }
1101 LodSelection::Level(level) => {
1102 validate_level(level)?;
1103 let max = level
1104 .checked_add(1)
1105 .ok_or_else(|| Error::InvalidInput(format!("LOD level {level} is too large")))?;
1106 Ok((level, max))
1107 }
1108 LodSelection::LevelMinMax(min, max) => {
1109 validate_level(min)?;
1110 validate_level(max)?;
1111 if max < min {
1112 return Err(Error::InvalidInput(format!(
1113 "LOD max {max} is smaller than min {min}"
1114 )));
1115 }
1116 Ok((min, max))
1117 }
1118 }
1119}
1120
1121fn validate_level(level: i32) -> Result<()> {
1122 if level < 0 {
1123 return Err(Error::InvalidInput(format!(
1124 "LOD level must be non-negative, got {level}"
1125 )));
1126 }
1127 Ok(())
1128}
1129
1130pub(crate) fn total_candidate_points(entries: &[Entry]) -> Result<usize> {
1131 entries.iter().try_fold(0usize, |total, entry| {
1132 let count = usize::try_from(entry.point_count).map_err(|_| {
1133 Error::InvalidData(format!(
1134 "negative point count {} for {:?}",
1135 entry.point_count, entry.key
1136 ))
1137 })?;
1138 total
1139 .checked_add(count)
1140 .ok_or_else(|| Error::InvalidData("selected point count overflows usize".into()))
1141 })
1142}
1143
1144pub(crate) fn voxel_bounds(key: copc_core::VoxelKey, info: &CopcInfo) -> Result<Bounds> {
1145 key.validate()?;
1146 let side = (info.halfsize * 2.0) / 2.0_f64.powi(key.level);
1147 let root_min = (
1148 info.center.0 - info.halfsize,
1149 info.center.1 - info.halfsize,
1150 info.center.2 - info.halfsize,
1151 );
1152 let min = (
1153 root_min.0 + f64::from(key.x) * side,
1154 root_min.1 + f64::from(key.y) * side,
1155 root_min.2 + f64::from(key.z) * side,
1156 );
1157 Ok(Bounds::new(min, (min.0 + side, min.1 + side, min.2 + side)))
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162 use super::*;
1163
1164 #[test]
1165 fn selected_column_builders_include_extra_bytes_width() {
1166 let mut format = LasPointFormat::new(6).unwrap();
1167 format.extra_bytes = 3;
1168
1169 let columns = selected_column_builders(format, ColumnSelection::all(), 2).unwrap();
1170
1171 let extra_spec = columns
1172 .iter()
1173 .map(|(spec, _)| *spec)
1174 .find(|spec| spec.dimension == LasDimension::ExtraBytes)
1175 .expect("ExtraBytes column spec");
1176 assert_eq!(Some(3), extra_spec.extra_byte_width());
1177 assert_eq!(copc_core::ScalarType::U8, extra_spec.scalar);
1178 }
1179
1180 fn record_bytes(format: &LasPointFormat, raw_point: &las::raw::Point) -> Vec<u8> {
1181 let mut buf = Vec::with_capacity(usize::from(format.len()));
1182 raw_point.write_to(&mut buf, format).unwrap();
1183 buf
1184 }
1185
1186 #[test]
1187 fn append_columns_preserves_fixed_width_extra_bytes() {
1188 let mut format = LasPointFormat::new(6).unwrap();
1189 format.extra_bytes = 3;
1190 let mut columns = selected_column_builders(
1191 format,
1192 ColumnSelection::from_dimensions([LasDimension::X, LasDimension::ExtraBytes]),
1193 1,
1194 )
1195 .unwrap();
1196 let raw_point = las::raw::Point {
1197 x: 10,
1198 y: 20,
1199 z: 30,
1200 flags: las::raw::point::Flags::ThreeByte(1 | (1 << 4), 0, 2),
1201 scan_angle: las::raw::point::ScanAngle::from(0.0),
1202 extra_bytes: vec![9, 8, 7],
1203 ..Default::default()
1204 };
1205 let buf = record_bytes(&format, &raw_point);
1206 let layout = ExtendedPointLayout::for_format(&format).unwrap();
1207
1208 append_columns(&mut columns, &buf, &layout, (1.0, 2.0, 3.0)).unwrap();
1209 let batch = LasColumnBatch::new(columns).unwrap();
1210
1211 assert_eq!(1, batch.len());
1212 assert_eq!(
1213 Some(&ColumnData::U8(vec![9, 8, 7])),
1214 batch.column(LasDimension::ExtraBytes)
1215 );
1216 }
1217
1218 #[test]
1219 fn append_columns_rejects_layout_and_spec_width_mismatch() {
1220 let mut spec_format = LasPointFormat::new(6).unwrap();
1223 spec_format.extra_bytes = 3;
1224 let mut columns = selected_column_builders(
1225 spec_format,
1226 ColumnSelection::from_dimensions([LasDimension::ExtraBytes]),
1227 1,
1228 )
1229 .unwrap();
1230 let mut buf_format = LasPointFormat::new(6).unwrap();
1231 buf_format.extra_bytes = 2;
1232 let raw_point = las::raw::Point {
1233 flags: las::raw::point::Flags::ThreeByte(1 | (1 << 4), 0, 2),
1234 extra_bytes: vec![9, 8],
1235 ..Default::default()
1236 };
1237 let buf = record_bytes(&buf_format, &raw_point);
1238 let layout = ExtendedPointLayout::for_format(&buf_format).unwrap();
1239
1240 let err = append_columns(&mut columns, &buf, &layout, (1.0, 2.0, 3.0)).unwrap_err();
1241
1242 assert!(err.to_string().contains("expected 3"));
1243 }
1244
1245 #[test]
1248 fn direct_column_decode_matches_las_raw_point() {
1249 for format_id in [6u8, 7, 8] {
1250 let mut format = LasPointFormat::new(format_id).unwrap();
1251 format.extra_bytes = 2;
1252 let raw_point = las::raw::Point {
1253 x: -1234,
1254 y: 5678,
1255 z: 91011,
1256 intensity: 0xBEEF,
1257 flags: las::raw::point::Flags::ThreeByte(
1258 3 | (5 << 4),
1259 1 | (1 << 2) | (2 << 4) | (1 << 6) | (1 << 7),
1260 6,
1261 ),
1262 scan_angle: las::raw::point::ScanAngle::Scaled(-5042),
1263 user_data: 0x42,
1264 point_source_id: 0xCAFE,
1265 gps_time: Some(1.234e9),
1266 color: format
1267 .has_color
1268 .then_some(las::Color::new(1000, 2000, 3000)),
1269 nir: format.has_nir.then_some(0xCDCD),
1270 waveform: None,
1271 extra_bytes: vec![7, 9],
1272 };
1273 let buf = record_bytes(&format, &raw_point);
1274 let layout = ExtendedPointLayout::for_format(&format).unwrap();
1275 let mut columns = selected_column_builders(format, ColumnSelection::all(), 1).unwrap();
1276
1277 append_columns(&mut columns, &buf, &layout, (1.0, 2.0, 3.0)).unwrap();
1278 let batch = LasColumnBatch::new(columns).unwrap();
1279
1280 let mut flags = raw_point.flags;
1281 let expected_overlap = flags.is_overlap();
1282 flags.clear_overlap_class();
1283 assert_eq!(
1284 Some(&ColumnData::U16(vec![raw_point.intensity])),
1285 batch.column(LasDimension::Intensity)
1286 );
1287 assert_eq!(
1288 Some(&ColumnData::U8(vec![flags.return_number()])),
1289 batch.column(LasDimension::ReturnNumber)
1290 );
1291 assert_eq!(
1292 Some(&ColumnData::U8(vec![flags.number_of_returns()])),
1293 batch.column(LasDimension::NumberOfReturns)
1294 );
1295 assert_eq!(
1296 Some(&ColumnData::U8(vec![u8::from(
1297 flags.to_classification().unwrap()
1298 )])),
1299 batch.column(LasDimension::Classification)
1300 );
1301 assert_eq!(
1302 Some(&ColumnData::Bool(vec![matches!(
1303 flags.scan_direction(),
1304 las::point::ScanDirection::LeftToRight
1305 )])),
1306 batch.column(LasDimension::ScanDirectionFlag)
1307 );
1308 assert_eq!(
1309 Some(&ColumnData::Bool(vec![flags.is_edge_of_flight_line()])),
1310 batch.column(LasDimension::EdgeOfFlightLine)
1311 );
1312 assert_eq!(
1313 Some(&ColumnData::F32(vec![f32::from(raw_point.scan_angle)])),
1314 batch.column(LasDimension::ScanAngle)
1315 );
1316 assert_eq!(
1317 Some(&ColumnData::U8(vec![raw_point.user_data])),
1318 batch.column(LasDimension::UserData)
1319 );
1320 assert_eq!(
1321 Some(&ColumnData::U16(vec![raw_point.point_source_id])),
1322 batch.column(LasDimension::PointSourceId)
1323 );
1324 assert_eq!(
1325 Some(&ColumnData::Bool(vec![flags.is_synthetic()])),
1326 batch.column(LasDimension::Synthetic)
1327 );
1328 assert_eq!(
1329 Some(&ColumnData::Bool(vec![flags.is_key_point()])),
1330 batch.column(LasDimension::KeyPoint)
1331 );
1332 assert_eq!(
1333 Some(&ColumnData::Bool(vec![flags.is_withheld()])),
1334 batch.column(LasDimension::Withheld)
1335 );
1336 assert_eq!(
1337 Some(&ColumnData::Bool(vec![expected_overlap])),
1338 batch.column(LasDimension::Overlap)
1339 );
1340 assert_eq!(
1341 Some(&ColumnData::U8(vec![flags.scanner_channel()])),
1342 batch.column(LasDimension::ScanChannel)
1343 );
1344 assert_eq!(
1345 Some(&ColumnData::F64(vec![raw_point.gps_time.unwrap()])),
1346 batch.column(LasDimension::GpsTime)
1347 );
1348 if format.has_color {
1349 assert_eq!(
1350 Some(&ColumnData::U16(vec![raw_point.color.unwrap().red])),
1351 batch.column(LasDimension::Red)
1352 );
1353 assert_eq!(
1354 Some(&ColumnData::U16(vec![raw_point.color.unwrap().blue])),
1355 batch.column(LasDimension::Blue)
1356 );
1357 }
1358 if format.has_nir {
1359 assert_eq!(
1360 Some(&ColumnData::U16(vec![raw_point.nir.unwrap()])),
1361 batch.column(LasDimension::Nir)
1362 );
1363 }
1364 assert_eq!(
1365 Some(&ColumnData::U8(vec![7, 9])),
1366 batch.column(LasDimension::ExtraBytes)
1367 );
1368 }
1369 }
1370}