1use std::fs::File;
2use std::io::{BufReader, BufWriter, Cursor, Seek, SeekFrom, Write};
3use std::path::Path;
4
5use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
6use copc_core::{
7 Bounds, CancelCheck, ColumnData, CopcInfo, Entry, Error, LasColumnBatch, LasDimension,
8 LasPointRecord, NeverCancel, Result, StreamingLayout, VoxelKey, HIERARCHY_ENTRY_BYTES,
9};
10use las::{point::Format as LasFormat, raw, Color, Read as _};
11use laz::{LasZipCompressor, LazVlrBuilder};
12use tempfile::{NamedTempFile, TempPath};
13
14use crate::spill::{SpillReader, SpillWriter};
15
16const CANCEL_POLL_STRIDE: usize = 4_096;
17const HIERARCHY_PAGE_MAX_ENTRIES: usize = 4_096;
18const INDEX_RECORD_BYTES: u64 = 4;
19const MIN_LEAF_DEPTH: u32 = 21;
29const MAX_LEAF_DEPTH: u32 = 30;
30const LAS_14_SCAN_ANGLE_SCALE: f32 = 0.006;
31const LAS_VLR_HEADER_BYTES: u32 = 54;
32const LAS_EVLR_HEADER_BYTES: u64 = 60;
33const LASZIP_VLR_USER_ID: &str = "laszip encoded";
34const LASZIP_VLR_RECORD_ID: u16 = 22204;
35const LASF_PROJECTION_USER_ID: &str = "LASF_Projection";
36const WKT_CRS_RECORD_ID: u16 = 2112;
37const GEOTIFF_GEO_KEY_DIRECTORY_RECORD_ID: u16 = 34735;
38const GEOTIFF_DOUBLE_PARAMS_RECORD_ID: u16 = 34736;
39const GEOTIFF_ASCII_PARAMS_RECORD_ID: u16 = 34737;
40const LASF_SPEC_USER_ID: &str = "LASF_Spec";
41const EXTRA_BYTES_RECORD_ID: u16 = 4;
42const WKT_GLOBAL_ENCODING_BIT: u16 = 16;
43
44#[derive(Clone, Debug, PartialEq)]
46pub struct CopcPointFields {
47 pub x: f64,
48 pub y: f64,
49 pub z: f64,
50 pub intensity: u16,
51 pub return_number: u8,
52 pub number_of_returns: u8,
53 pub synthetic: u8,
54 pub key_point: u8,
55 pub withheld: u8,
56 pub overlap: u8,
57 pub scan_channel: u8,
58 pub scan_direction_flag: u8,
59 pub edge_of_flight_line: u8,
60 pub classification: u8,
61 pub user_data: u8,
62 pub scan_angle: f32,
64 pub point_source_id: u16,
65 pub gps_time: f64,
66 pub red: u16,
67 pub green: u16,
68 pub blue: u16,
69 pub extra_bytes: Vec<u8>,
70}
71
72pub trait CopcPointSource {
74 fn len(&self) -> usize;
75 fn xyz(&self, index: usize) -> (f64, f64, f64);
76 fn fields(&self, index: usize) -> Result<CopcPointFields>;
77 fn extra_byte_count(&self) -> u16 {
78 0
79 }
80 fn extra_bytes_vlrs(&self) -> &[las::Vlr] {
81 &[]
82 }
83
84 fn is_empty(&self) -> bool {
85 self.len() == 0
86 }
87}
88
89pub struct ColumnBatchSource<'a> {
91 batch: &'a LasColumnBatch,
92 x: &'a [f64],
93 y: &'a [f64],
94 z: &'a [f64],
95 intensity: Option<&'a [u16]>,
96 return_number: Option<&'a [u8]>,
97 number_of_returns: Option<&'a [u8]>,
98 synthetic: Option<&'a [bool]>,
99 key_point: Option<&'a [bool]>,
100 withheld: Option<&'a [bool]>,
101 overlap: Option<&'a [bool]>,
102 scan_channel: Option<&'a [u8]>,
103 scan_direction_flag: Option<&'a [bool]>,
104 edge_of_flight_line: Option<&'a [bool]>,
105 classification: Option<&'a [u8]>,
106 user_data: Option<&'a [u8]>,
107 scan_angle_rank: Option<&'a [i16]>,
108 point_source_id: Option<&'a [u16]>,
109 gps_time: Option<&'a [f64]>,
110 red: Option<&'a [u16]>,
111 green: Option<&'a [u16]>,
112 blue: Option<&'a [u16]>,
113 extra_bytes: Option<(&'a [u8], usize)>,
114}
115
116impl<'a> ColumnBatchSource<'a> {
117 pub fn new(batch: &'a LasColumnBatch) -> Result<Self> {
118 batch.validate()?;
119 validate_column_batch_writer_support(batch)?;
120
121 let x = required_f64_column(batch, LasDimension::X)?;
122 let y = required_f64_column(batch, LasDimension::Y)?;
123 let z = required_f64_column(batch, LasDimension::Z)?;
124 let red = optional_u16_column(batch, LasDimension::Red)?;
125 let green = optional_u16_column(batch, LasDimension::Green)?;
126 let blue = optional_u16_column(batch, LasDimension::Blue)?;
127 let extra_bytes = optional_extra_bytes_column(batch)?;
128 validate_color_columns(red, green, blue)?;
129
130 Ok(Self {
131 batch,
132 x,
133 y,
134 z,
135 intensity: optional_u16_column(batch, LasDimension::Intensity)?,
136 return_number: optional_u8_column(batch, LasDimension::ReturnNumber)?,
137 number_of_returns: optional_u8_column(batch, LasDimension::NumberOfReturns)?,
138 synthetic: optional_bool_column(batch, LasDimension::Synthetic)?,
139 key_point: optional_bool_column(batch, LasDimension::KeyPoint)?,
140 withheld: optional_bool_column(batch, LasDimension::Withheld)?,
141 overlap: optional_bool_column(batch, LasDimension::Overlap)?,
142 scan_channel: optional_u8_column(batch, LasDimension::ScanChannel)?,
143 scan_direction_flag: optional_bool_column(batch, LasDimension::ScanDirectionFlag)?,
144 edge_of_flight_line: optional_bool_column(batch, LasDimension::EdgeOfFlightLine)?,
145 classification: optional_u8_column(batch, LasDimension::Classification)?,
146 user_data: optional_u8_column(batch, LasDimension::UserData)?,
147 scan_angle_rank: optional_i16_column(batch, LasDimension::ScanAngleRank)?,
148 point_source_id: optional_u16_column(batch, LasDimension::PointSourceId)?,
149 gps_time: optional_f64_column(batch, LasDimension::GpsTime)?,
150 red,
151 green,
152 blue,
153 extra_bytes,
154 })
155 }
156
157 pub fn batch(&self) -> &LasColumnBatch {
158 self.batch
159 }
160
161 pub fn has_color(&self) -> bool {
162 self.red.is_some() && self.green.is_some() && self.blue.is_some()
163 }
164
165 pub fn extra_byte_width(&self) -> usize {
166 self.extra_bytes.map(|(_, width)| width).unwrap_or(0)
167 }
168
169 pub fn bounds(&self) -> Result<Bounds> {
170 if self.is_empty() {
171 return Err(Error::InvalidInput(
172 "cannot compute bounds for empty column batch".into(),
173 ));
174 }
175 let mut bounds = Bounds::point(self.x[0], self.y[0], self.z[0]);
176 for index in 1..self.len() {
177 bounds.extend(self.x[index], self.y[index], self.z[index]);
178 }
179 Ok(bounds)
180 }
181}
182
183impl CopcPointSource for ColumnBatchSource<'_> {
184 fn len(&self) -> usize {
185 self.batch.len()
186 }
187
188 #[inline]
189 fn xyz(&self, index: usize) -> (f64, f64, f64) {
190 (self.x[index], self.y[index], self.z[index])
191 }
192
193 fn fields(&self, index: usize) -> Result<CopcPointFields> {
194 Ok(CopcPointFields {
195 x: self.x[index],
196 y: self.y[index],
197 z: self.z[index],
198 intensity: at_u16(self.intensity, index),
199 return_number: at_u8(self.return_number, index),
200 number_of_returns: at_u8(self.number_of_returns, index),
201 synthetic: at_bool_u8(self.synthetic, index),
202 key_point: at_bool_u8(self.key_point, index),
203 withheld: at_bool_u8(self.withheld, index),
204 overlap: at_bool_u8(self.overlap, index),
205 scan_channel: at_u8(self.scan_channel, index),
206 scan_direction_flag: at_bool_u8(self.scan_direction_flag, index),
207 edge_of_flight_line: at_bool_u8(self.edge_of_flight_line, index),
208 classification: at_u8(self.classification, index),
209 user_data: at_u8(self.user_data, index),
210 scan_angle: self
211 .scan_angle_rank
212 .map(|column| column[index] as f32 * 90.0 / 180.0)
213 .unwrap_or(0.0),
214 point_source_id: at_u16(self.point_source_id, index),
215 gps_time: self.gps_time.map(|column| column[index]).unwrap_or(0.0),
216 red: at_u16(self.red, index),
217 green: at_u16(self.green, index),
218 blue: at_u16(self.blue, index),
219 extra_bytes: extra_bytes_at(self.extra_bytes, index),
220 })
221 }
222
223 fn extra_byte_count(&self) -> u16 {
224 self.extra_byte_width() as u16
225 }
226}
227
228fn at_bool_u8(column: Option<&[bool]>, index: usize) -> u8 {
229 column.map(|values| u8::from(values[index])).unwrap_or(0)
230}
231
232fn at_u8(column: Option<&[u8]>, index: usize) -> u8 {
233 column.map(|values| values[index]).unwrap_or(0)
234}
235
236fn at_u16(column: Option<&[u16]>, index: usize) -> u16 {
237 column.map(|values| values[index]).unwrap_or(0)
238}
239
240fn validate_column_batch_writer_support(batch: &LasColumnBatch) -> Result<()> {
241 let unsupported: Vec<_> = batch
242 .columns
243 .iter()
244 .filter_map(|(spec, _)| match spec.dimension {
245 LasDimension::Nir => Some("NIR point data"),
246 LasDimension::WaveformPacketDescriptorIndex
247 | LasDimension::WaveformPacketByteOffset
248 | LasDimension::WaveformPacketSize
249 | LasDimension::WavePacketReturnPointWaveformLocation => Some("waveform point data"),
250 _ => None,
251 })
252 .collect();
253 if unsupported.is_empty() {
254 Ok(())
255 } else {
256 Err(Error::Unsupported(format!(
257 "COPC writer cannot preserve {}",
258 unsupported.join(", ")
259 )))
260 }
261}
262
263fn validate_color_columns(
264 red: Option<&[u16]>,
265 green: Option<&[u16]>,
266 blue: Option<&[u16]>,
267) -> Result<()> {
268 let present =
269 usize::from(red.is_some()) + usize::from(green.is_some()) + usize::from(blue.is_some());
270 if present == 0 || present == 3 {
271 Ok(())
272 } else {
273 Err(Error::InvalidInput(
274 "Red, Green, and Blue columns must be supplied together".into(),
275 ))
276 }
277}
278
279fn required_f64_column(batch: &LasColumnBatch, dimension: LasDimension) -> Result<&[f64]> {
280 match batch.column(dimension) {
281 Some(ColumnData::F64(values)) => Ok(values),
282 Some(other) => Err(unexpected_column_type(dimension, "F64", other)),
283 None => Err(Error::InvalidInput(format!(
284 "ColumnBatchSource requires {dimension:?} column"
285 ))),
286 }
287}
288
289fn optional_f64_column(batch: &LasColumnBatch, dimension: LasDimension) -> Result<Option<&[f64]>> {
290 match batch.column(dimension) {
291 Some(ColumnData::F64(values)) => Ok(Some(values)),
292 Some(other) => Err(unexpected_column_type(dimension, "F64", other)),
293 None => Ok(None),
294 }
295}
296
297fn optional_i16_column(batch: &LasColumnBatch, dimension: LasDimension) -> Result<Option<&[i16]>> {
298 match batch.column(dimension) {
299 Some(ColumnData::I16(values)) => Ok(Some(values)),
300 Some(other) => Err(unexpected_column_type(dimension, "I16", other)),
301 None => Ok(None),
302 }
303}
304
305fn optional_u16_column(batch: &LasColumnBatch, dimension: LasDimension) -> Result<Option<&[u16]>> {
306 match batch.column(dimension) {
307 Some(ColumnData::U16(values)) => Ok(Some(values)),
308 Some(other) => Err(unexpected_column_type(dimension, "U16", other)),
309 None => Ok(None),
310 }
311}
312
313fn optional_u8_column(batch: &LasColumnBatch, dimension: LasDimension) -> Result<Option<&[u8]>> {
314 match batch.column(dimension) {
315 Some(ColumnData::U8(values)) => Ok(Some(values)),
316 Some(other) => Err(unexpected_column_type(dimension, "U8", other)),
317 None => Ok(None),
318 }
319}
320
321fn optional_extra_bytes_column(batch: &LasColumnBatch) -> Result<Option<(&[u8], usize)>> {
322 let mut extra_bytes = None;
323 for (spec, data) in &batch.columns {
324 if spec.dimension != LasDimension::ExtraBytes {
325 continue;
326 }
327 let width = spec.extra_byte_width().ok_or_else(|| {
328 Error::InvalidInput("ExtraBytes column requires a non-zero byte width".into())
329 })?;
330 if width > usize::from(u16::MAX) {
331 return Err(Error::InvalidInput(format!(
332 "ExtraBytes column width {width} exceeds LAS u16 range"
333 )));
334 }
335 let values = match data {
336 ColumnData::U8(values) => values.as_slice(),
337 other => {
338 return Err(unexpected_column_type(
339 LasDimension::ExtraBytes,
340 "U8",
341 other,
342 ))
343 }
344 };
345 if extra_bytes.replace((values, width)).is_some() {
346 return Err(Error::InvalidInput(
347 "ColumnBatchSource supports at most one ExtraBytes column".into(),
348 ));
349 }
350 }
351 Ok(extra_bytes)
352}
353
354fn extra_bytes_at(column: Option<(&[u8], usize)>, index: usize) -> Vec<u8> {
355 match column {
356 Some((values, width)) => {
357 let start = index * width;
358 values[start..start + width].to_vec()
359 }
360 None => Vec::new(),
361 }
362}
363
364fn optional_bool_column(
365 batch: &LasColumnBatch,
366 dimension: LasDimension,
367) -> Result<Option<&[bool]>> {
368 match batch.column(dimension) {
369 Some(ColumnData::Bool(values)) => Ok(Some(values)),
370 Some(other) => Err(unexpected_column_type(dimension, "Bool", other)),
371 None => Ok(None),
372 }
373}
374
375fn unexpected_column_type(dimension: LasDimension, expected: &str, actual: &ColumnData) -> Error {
376 Error::InvalidInput(format!(
377 "{dimension:?} column must be {expected}, found {:?}",
378 actual.scalar()
379 ))
380}
381
382struct SpillSource<'a> {
383 reader: &'a SpillReader,
384}
385
386impl CopcPointSource for SpillSource<'_> {
387 fn len(&self) -> usize {
388 self.reader.len()
389 }
390
391 #[inline]
392 fn xyz(&self, index: usize) -> (f64, f64, f64) {
393 self.reader.xyz_at(index)
394 }
395
396 fn fields(&self, index: usize) -> Result<CopcPointFields> {
397 let record = self.reader.record_at(index)?;
398 Ok(CopcPointFields {
399 x: record.x,
400 y: record.y,
401 z: record.z,
402 intensity: record.intensity,
403 return_number: record.return_number,
404 number_of_returns: record.number_of_returns,
405 synthetic: u8::from(record.synthetic),
406 key_point: u8::from(record.key_point),
407 withheld: u8::from(record.withheld),
408 overlap: u8::from(record.overlap),
409 scan_channel: record.scan_channel,
410 scan_direction_flag: u8::from(record.scan_direction_flag),
411 edge_of_flight_line: u8::from(record.edge_of_flight_line),
412 classification: record.classification,
413 user_data: record.user_data,
414 scan_angle: record.scan_angle,
415 point_source_id: record.point_source_id,
416 gps_time: record.gps_time,
417 red: record.red,
418 green: record.green,
419 blue: record.blue,
420 extra_bytes: record.extra_bytes,
421 })
422 }
423
424 fn extra_byte_count(&self) -> u16 {
425 self.reader.layout().extra_bytes
426 }
427
428 fn extra_bytes_vlrs(&self) -> &[las::Vlr] {
429 &self.reader.layout().extra_bytes_descriptors
430 }
431}
432
433#[derive(Clone, Debug)]
434struct OutputCrsRecord {
435 vlr: las::Vlr,
436 is_extended: bool,
437}
438
439#[derive(Clone, Debug)]
440struct OutputLasMetadata {
441 file_source_id: u16,
442 global_encoding: u16,
443 guid: [u8; 16],
444 system_identifier: String,
445 generating_software: String,
446 creation_day_of_year: u16,
447 creation_year: u16,
448 scale: (f64, f64, f64),
449 offset: Option<(f64, f64, f64)>,
450 crs_records: Vec<OutputCrsRecord>,
451 pass_through_vlrs: Vec<las::Vlr>,
452 pass_through_evlrs: Vec<las::Vlr>,
453}
454
455impl Default for OutputLasMetadata {
456 fn default() -> Self {
457 Self {
458 file_source_id: 0,
459 global_encoding: 0,
460 guid: [0; 16],
461 system_identifier: "copc-rust".to_string(),
462 generating_software: "copc-writer".to_string(),
463 creation_day_of_year: 0,
464 creation_year: 2026,
465 scale: (0.001, 0.001, 0.001),
466 offset: None,
467 crs_records: Vec::new(),
468 pass_through_vlrs: Vec::new(),
469 pass_through_evlrs: Vec::new(),
470 }
471 }
472}
473
474impl OutputLasMetadata {
475 fn from_las_header(header: &las::Header) -> Self {
476 let mut global_encoding = u16::from(header.gps_time_type());
477 if header.has_synthetic_return_numbers() {
478 global_encoding |= 8;
479 }
480 let crs_records = extract_source_wkt_crs_records(header);
481 if !crs_records.is_empty() {
482 global_encoding |= WKT_GLOBAL_ENCODING_BIT;
483 }
484 let pass_through_vlrs = extract_pass_through_vlrs(header);
485 let pass_through_evlrs = extract_pass_through_evlrs(header);
486 let transforms = header.transforms();
487 let (creation_day_of_year, creation_year) = header
488 .date()
489 .map(|date| {
490 let year = date.format("%Y").to_string().parse().unwrap_or(0);
491 let day = date.format("%j").to_string().parse().unwrap_or(0);
492 (day, year)
493 })
494 .unwrap_or((0, 0));
495
496 Self {
497 file_source_id: header.file_source_id(),
498 global_encoding,
499 guid: *header.guid().as_bytes(),
500 system_identifier: header.system_identifier().to_string(),
501 generating_software: header.generating_software().to_string(),
502 creation_day_of_year,
503 creation_year,
504 scale: (transforms.x.scale, transforms.y.scale, transforms.z.scale),
505 offset: Some((
506 transforms.x.offset,
507 transforms.y.offset,
508 transforms.z.offset,
509 )),
510 crs_records,
511 pass_through_vlrs,
512 pass_through_evlrs,
513 }
514 }
515
516 fn regular_crs_vlrs(&self) -> impl Iterator<Item = &las::Vlr> {
517 self.crs_records
518 .iter()
519 .filter(|record| !record.is_extended)
520 .map(|record| &record.vlr)
521 }
522
523 fn extended_crs_evlrs(&self) -> impl Iterator<Item = &las::Vlr> {
524 self.crs_records
525 .iter()
526 .filter(|record| record.is_extended)
527 .map(|record| &record.vlr)
528 }
529
530 fn regular_crs_vlr_count(&self) -> usize {
531 self.crs_records
532 .iter()
533 .filter(|record| !record.is_extended)
534 .count()
535 }
536
537 fn extended_crs_evlr_count(&self) -> usize {
538 self.crs_records
539 .iter()
540 .filter(|record| record.is_extended)
541 .count()
542 }
543
544 fn regular_crs_vlr_bytes(&self) -> Result<u32> {
545 self.regular_crs_vlrs().try_fold(0u32, |total, vlr| {
546 let data_len = u16::try_from(vlr.data.len()).map_err(|_| {
547 Error::InvalidInput(format!(
548 "regular WKT CRS VLR is too large: {} byte(s)",
549 vlr.data.len()
550 ))
551 })?;
552 total
553 .checked_add(LAS_VLR_HEADER_BYTES + u32::from(data_len))
554 .ok_or_else(|| Error::InvalidInput("CRS VLR byte size overflow".into()))
555 })
556 }
557
558 fn source_evlrs_after_hierarchy(&self) -> impl Iterator<Item = &las::Vlr> {
559 self.extended_crs_evlrs()
560 .chain(self.pass_through_evlrs.iter())
561 }
562
563 fn source_evlr_count_after_hierarchy(&self) -> usize {
564 self.extended_crs_evlr_count() + self.pass_through_evlrs.len()
565 }
566}
567
568#[derive(Clone, Copy, Debug, PartialEq)]
569struct PointStats {
570 gpstime_min: f64,
571 gpstime_max: f64,
572 extended_return_counts: [u64; 15],
573}
574
575impl PointStats {
576 fn new() -> Self {
577 Self {
578 gpstime_min: f64::INFINITY,
579 gpstime_max: f64::NEG_INFINITY,
580 extended_return_counts: [0; 15],
581 }
582 }
583
584 fn record(&mut self, index: usize, fields: &CopcPointFields) -> Result<()> {
585 validate_finite_value(&format!("point {index} GPS time"), fields.gps_time)?;
586 self.gpstime_min = self.gpstime_min.min(fields.gps_time);
587 self.gpstime_max = self.gpstime_max.max(fields.gps_time);
588 if (1..=15).contains(&fields.return_number) {
589 self.extended_return_counts[usize::from(fields.return_number - 1)] += 1;
590 }
591 Ok(())
592 }
593}
594
595#[derive(Debug, Clone, Copy)]
596pub struct CopcWriterParams {
597 pub max_points_per_node: u32,
598 pub max_depth: u32,
599}
600
601impl Default for CopcWriterParams {
602 fn default() -> Self {
603 Self {
604 max_points_per_node: 100_000,
605 max_depth: 8,
606 }
607 }
608}
609
610pub fn write_source<S: CopcPointSource>(
611 path: &Path,
612 source: &S,
613 has_color: bool,
614 bounds: Bounds,
615 params: &CopcWriterParams,
616) -> Result<()> {
617 write_source_with_cancel(path, source, has_color, bounds, params, &NeverCancel)
618}
619
620pub fn write_source_with_cancel<S: CopcPointSource>(
621 path: &Path,
622 source: &S,
623 has_color: bool,
624 bounds: Bounds,
625 params: &CopcWriterParams,
626 cancel: &dyn CancelCheck,
627) -> Result<()> {
628 cancel.check()?;
629 if source.is_empty() {
630 return Err(Error::InvalidInput(
631 "cannot write empty cloud to COPC".into(),
632 ));
633 }
634 write_copc_inner(
635 path,
636 source,
637 has_color,
638 bounds,
639 params,
640 cancel,
641 &OutputLasMetadata::default(),
642 )
643}
644
645pub fn write_streaming_with_cancel<I>(
646 path: &Path,
647 layout: StreamingLayout,
648 points: I,
649 params: &CopcWriterParams,
650 spill_dir: &Path,
651 cancel: &dyn CancelCheck,
652) -> Result<()>
653where
654 I: IntoIterator<Item = Result<LasPointRecord>>,
655{
656 cancel.check()?;
657 validate_streaming_layout_supported(&layout)?;
658 let mut spill = SpillWriter::create(spill_dir, layout)?;
659 for (index, item) in points.into_iter().enumerate() {
660 if index % CANCEL_POLL_STRIDE == 0 {
661 cancel.check()?;
662 }
663 let record = item?;
664 validate_record_coordinates(&record, index)?;
665 spill.push(&record)?;
666 }
667 cancel.check()?;
668 let reader = spill.finalize()?;
669 write_copc_from_spill(path, reader, params, cancel, &OutputLasMetadata::default())
670}
671
672pub fn convert_las_to_copc_streaming(
673 las_path: &Path,
674 copc_path: &Path,
675 params: &CopcWriterParams,
676 spill_dir: &Path,
677 cancel: &dyn CancelCheck,
678) -> Result<()> {
679 cancel.check()?;
680 let mut reader = las::Reader::from_path(las_path).map_err(|e| Error::Las(e.to_string()))?;
681 validate_las_conversion_supported(reader.header())?;
682 let output_metadata = OutputLasMetadata::from_las_header(reader.header());
683 let layout = StreamingLayout::from_las_header(reader.header());
684 let mut spill = SpillWriter::create(spill_dir, layout)?;
685 for (index, result) in reader.points().enumerate() {
686 if index % CANCEL_POLL_STRIDE == 0 {
687 cancel.check()?;
688 }
689 let point = result.map_err(|e| Error::Las(e.to_string()))?;
690 let record = LasPointRecord::from_las_point(&point);
691 validate_record_coordinates(&record, index)?;
692 spill.push(&record)?;
693 }
694 cancel.check()?;
695 let reader = spill.finalize()?;
696 write_copc_from_spill(copc_path, reader, params, cancel, &output_metadata)
697}
698
699fn validate_streaming_layout_supported(layout: &StreamingLayout) -> Result<()> {
700 let mut unsupported = Vec::new();
701 if layout.has_nir {
702 unsupported.push("NIR point data".to_string());
703 }
704 if layout.has_waveform {
705 unsupported.push("waveform point data".to_string());
706 }
707 if unsupported.is_empty() {
708 Ok(())
709 } else {
710 Err(Error::Unsupported(format!(
711 "COPC writer cannot preserve {}",
712 unsupported.join(", ")
713 )))
714 }
715}
716
717fn validate_las_conversion_supported(header: &las::Header) -> Result<()> {
718 let mut unsupported = Vec::new();
719 let format = header.point_format();
720 if format.has_nir {
721 unsupported.push("NIR point data".to_string());
722 }
723 if format.has_waveform {
724 unsupported.push("waveform point data".to_string());
725 }
726 let source_has_wkt_crs_record = has_wkt_crs_record(header);
727 let mut geotiff_crs_record_count = 0usize;
728 for vlr in header.vlrs() {
729 if is_geotiff_crs_vlr(vlr) && !source_has_wkt_crs_record {
730 geotiff_crs_record_count += 1;
731 }
732 }
733 for evlr in header.evlrs() {
734 if is_geotiff_crs_vlr(evlr) && !source_has_wkt_crs_record {
735 geotiff_crs_record_count += 1;
736 }
737 }
738 if geotiff_crs_record_count > 0 {
739 unsupported.push(format!(
740 "{geotiff_crs_record_count} GeoTIFF CRS VLR/EVLR(s); GeoTIFF-to-WKT CRS conversion is not implemented in copc-writer"
741 ));
742 }
743
744 if unsupported.is_empty() {
745 Ok(())
746 } else {
747 Err(Error::Unsupported(format!(
748 "LAS-to-COPC streaming conversion cannot preserve {}",
749 unsupported.join(", ")
750 )))
751 }
752}
753
754fn is_laszip_vlr(vlr: &las::Vlr) -> bool {
755 vlr.user_id == LASZIP_VLR_USER_ID && vlr.record_id == LASZIP_VLR_RECORD_ID
756}
757
758fn is_copc_info_vlr(vlr: &las::Vlr) -> bool {
759 vlr.user_id == "copc" && vlr.record_id == 1
760}
761
762fn is_copc_hierarchy_evlr(vlr: &las::Vlr) -> bool {
763 vlr.user_id == "copc" && vlr.record_id == 1000
764}
765
766fn is_wkt_crs_vlr(vlr: &las::Vlr) -> bool {
767 vlr.user_id == LASF_PROJECTION_USER_ID && vlr.record_id == WKT_CRS_RECORD_ID
768}
769
770fn is_geotiff_crs_vlr(vlr: &las::Vlr) -> bool {
771 vlr.user_id == LASF_PROJECTION_USER_ID
772 && matches!(
773 vlr.record_id,
774 GEOTIFF_GEO_KEY_DIRECTORY_RECORD_ID
775 | GEOTIFF_DOUBLE_PARAMS_RECORD_ID
776 | GEOTIFF_ASCII_PARAMS_RECORD_ID
777 )
778}
779
780fn is_extra_bytes_descriptor_vlr(vlr: &las::Vlr) -> bool {
781 vlr.user_id == LASF_SPEC_USER_ID && vlr.record_id == EXTRA_BYTES_RECORD_ID
782}
783
784fn has_wkt_crs_record(header: &las::Header) -> bool {
785 header.vlrs().iter().any(is_wkt_crs_vlr) || header.evlrs().iter().any(is_wkt_crs_vlr)
786}
787
788fn extract_source_wkt_crs_records(header: &las::Header) -> Vec<OutputCrsRecord> {
789 let mut records = Vec::new();
790 for vlr in header.vlrs() {
791 if is_wkt_crs_vlr(vlr) {
792 records.push(OutputCrsRecord {
793 vlr: vlr.clone(),
794 is_extended: false,
795 });
796 }
797 }
798 for evlr in header.evlrs() {
799 if is_wkt_crs_vlr(evlr) {
800 records.push(OutputCrsRecord {
801 vlr: evlr.clone(),
802 is_extended: true,
803 });
804 }
805 }
806 records
807}
808
809fn extract_pass_through_vlrs(header: &las::Header) -> Vec<las::Vlr> {
810 header
811 .vlrs()
812 .iter()
813 .filter(|vlr| !is_laszip_vlr(vlr))
814 .filter(|vlr| !is_copc_info_vlr(vlr))
815 .filter(|vlr| !is_copc_hierarchy_evlr(vlr))
816 .filter(|vlr| !is_wkt_crs_vlr(vlr))
817 .filter(|vlr| !is_geotiff_crs_vlr(vlr))
818 .filter(|vlr| !is_extra_bytes_descriptor_vlr(vlr))
819 .cloned()
820 .collect()
821}
822
823fn extract_pass_through_evlrs(header: &las::Header) -> Vec<las::Vlr> {
824 header
825 .evlrs()
826 .iter()
827 .filter(|evlr| !is_laszip_vlr(evlr))
828 .filter(|evlr| !is_copc_info_vlr(evlr))
829 .filter(|evlr| !is_copc_hierarchy_evlr(evlr))
830 .filter(|evlr| !is_wkt_crs_vlr(evlr))
831 .filter(|evlr| !is_geotiff_crs_vlr(evlr))
832 .cloned()
833 .collect()
834}
835
836fn validate_record_coordinates(record: &LasPointRecord, index: usize) -> Result<()> {
837 validate_xyz_finite(index, record.x, record.y, record.z)
838}
839
840fn validate_coordinate_inputs<S: CopcPointSource>(
841 source: &S,
842 bounds: Bounds,
843 scale: (f64, f64, f64),
844 offset: (f64, f64, f64),
845 cancel: &dyn CancelCheck,
846) -> Result<PointStats> {
847 validate_bounds(bounds)?;
848 validate_transform(scale, offset)?;
849 let extra_byte_count = usize::from(source.extra_byte_count());
850 let mut stats = PointStats::new();
851 for index in 0..source.len() {
852 if index % CANCEL_POLL_STRIDE == 0 {
853 cancel.check()?;
854 }
855 let (x, y, z) = source.xyz(index);
856 validate_xyz_finite(index, x, y, z)?;
857 quantize_xyz(index, x, y, z, scale, offset)?;
858
859 let fields = source.fields(index)?;
860 validate_xyz_finite(index, fields.x, fields.y, fields.z)?;
861 quantize_xyz(index, fields.x, fields.y, fields.z, scale, offset)?;
862 validate_scan_angle(index, fields.scan_angle)?;
863 validate_point_flags(index, &fields)?;
864 if fields.extra_bytes.len() != extra_byte_count {
865 return Err(Error::InvalidInput(format!(
866 "point {index} has {} extra byte(s), expected {extra_byte_count}",
867 fields.extra_bytes.len()
868 )));
869 }
870 stats.record(index, &fields)?;
871 }
872 Ok(stats)
873}
874
875fn validate_point_flags(index: usize, fields: &CopcPointFields) -> Result<()> {
876 validate_point_field_range(index, "return_number", fields.return_number, 0, 15)?;
877 validate_point_field_range(index, "number_of_returns", fields.number_of_returns, 0, 15)?;
878 validate_point_field_range(index, "synthetic", fields.synthetic, 0, 1)?;
879 validate_point_field_range(index, "key_point", fields.key_point, 0, 1)?;
880 validate_point_field_range(index, "withheld", fields.withheld, 0, 1)?;
881 validate_point_field_range(index, "overlap", fields.overlap, 0, 1)?;
882 validate_point_field_range(index, "scan_channel", fields.scan_channel, 0, 3)?;
883 validate_point_field_range(
884 index,
885 "scan_direction_flag",
886 fields.scan_direction_flag,
887 0,
888 1,
889 )?;
890 validate_point_field_range(
891 index,
892 "edge_of_flight_line",
893 fields.edge_of_flight_line,
894 0,
895 1,
896 )
897}
898
899fn validate_scan_angle(index: usize, value: f32) -> Result<()> {
900 if !value.is_finite() {
901 return Err(Error::InvalidInput(format!(
902 "point {index} scan_angle must be finite, got {value}"
903 )));
904 }
905 let scaled = value / LAS_14_SCAN_ANGLE_SCALE;
906 if scaled < f32::from(i16::MIN) || scaled > f32::from(i16::MAX) {
907 return Err(Error::InvalidInput(format!(
908 "point {index} scan_angle {value} encodes to {scaled}, outside LAS i16 range"
909 )));
910 }
911 Ok(())
912}
913
914fn validate_point_field_range(index: usize, name: &str, value: u8, min: u8, max: u8) -> Result<()> {
915 if (min..=max).contains(&value) {
916 Ok(())
917 } else {
918 Err(Error::InvalidInput(format!(
919 "point {index} {name} must be in {min}..={max}, got {value}"
920 )))
921 }
922}
923
924fn validate_bounds(bounds: Bounds) -> Result<()> {
925 validate_finite_value("bounds min x", bounds.min.0)?;
926 validate_finite_value("bounds min y", bounds.min.1)?;
927 validate_finite_value("bounds min z", bounds.min.2)?;
928 validate_finite_value("bounds max x", bounds.max.0)?;
929 validate_finite_value("bounds max y", bounds.max.1)?;
930 validate_finite_value("bounds max z", bounds.max.2)?;
931 for (axis, min, max) in [
932 ("x", bounds.min.0, bounds.max.0),
933 ("y", bounds.min.1, bounds.max.1),
934 ("z", bounds.min.2, bounds.max.2),
935 ] {
936 if min > max {
937 return Err(Error::InvalidInput(format!(
938 "bounds {axis} min {min} exceeds max {max}"
939 )));
940 }
941 validate_finite_value(&format!("bounds {axis} span"), max - min)?;
942 }
943 Ok(())
944}
945
946fn validate_transform(scale: (f64, f64, f64), offset: (f64, f64, f64)) -> Result<()> {
947 for (axis, value) in [("x", scale.0), ("y", scale.1), ("z", scale.2)] {
948 if !value.is_finite() || value <= 0.0 {
949 return Err(Error::InvalidInput(format!(
950 "LAS {axis} scale must be finite and positive, got {value}"
951 )));
952 }
953 }
954 validate_finite_value("LAS x offset", offset.0)?;
955 validate_finite_value("LAS y offset", offset.1)?;
956 validate_finite_value("LAS z offset", offset.2)?;
957 Ok(())
958}
959
960fn validate_xyz_finite(index: usize, x: f64, y: f64, z: f64) -> Result<()> {
961 validate_point_axis_finite(index, "x", x)?;
962 validate_point_axis_finite(index, "y", y)?;
963 validate_point_axis_finite(index, "z", z)
964}
965
966fn validate_point_axis_finite(index: usize, axis: &str, value: f64) -> Result<()> {
967 if value.is_finite() {
968 Ok(())
969 } else {
970 Err(Error::InvalidInput(format!(
971 "point {index} {axis} coordinate must be finite, got {value}"
972 )))
973 }
974}
975
976fn validate_finite_value(name: &str, value: f64) -> Result<()> {
977 if value.is_finite() {
978 Ok(())
979 } else {
980 Err(Error::InvalidInput(format!(
981 "{name} must be finite, got {value}"
982 )))
983 }
984}
985
986fn quantize_xyz(
987 index: usize,
988 x: f64,
989 y: f64,
990 z: f64,
991 scale: (f64, f64, f64),
992 offset: (f64, f64, f64),
993) -> Result<(i32, i32, i32)> {
994 Ok((
995 quantize_axis(index, "x", x, scale.0, offset.0)?,
996 quantize_axis(index, "y", y, scale.1, offset.1)?,
997 quantize_axis(index, "z", z, scale.2, offset.2)?,
998 ))
999}
1000
1001fn quantize_axis(index: usize, axis: &str, value: f64, scale: f64, offset: f64) -> Result<i32> {
1002 let scaled = ((value - offset) / scale).round();
1003 if !scaled.is_finite() {
1004 return Err(Error::InvalidInput(format!(
1005 "point {index} {axis} coordinate cannot be encoded with scale {scale} and offset {offset}"
1006 )));
1007 }
1008 if scaled < f64::from(i32::MIN) || scaled > f64::from(i32::MAX) {
1009 return Err(Error::InvalidInput(format!(
1010 "point {index} {axis} coordinate {value} encodes to {scaled}, outside LAS i32 range"
1011 )));
1012 }
1013 Ok(scaled as i32)
1014}
1015
1016fn write_copc_from_spill(
1017 path: &Path,
1018 reader: SpillReader,
1019 params: &CopcWriterParams,
1020 cancel: &dyn CancelCheck,
1021 metadata: &OutputLasMetadata,
1022) -> Result<()> {
1023 cancel.check()?;
1024 validate_streaming_layout_supported(reader.layout())?;
1025 if reader.is_empty() {
1026 return Err(Error::InvalidInput(
1027 "cannot write empty cloud to COPC".into(),
1028 ));
1029 }
1030 let has_color = reader.layout().has_color;
1031 let bounds = reader.bounds();
1032 let source = SpillSource { reader: &reader };
1033 write_copc_inner(path, &source, has_color, bounds, params, cancel, metadata)
1034}
1035
1036fn write_copc_inner<S: CopcPointSource>(
1037 path: &Path,
1038 source: &S,
1039 has_color: bool,
1040 bounds: Bounds,
1041 params: &CopcWriterParams,
1042 cancel: &dyn CancelCheck,
1043 metadata: &OutputLasMetadata,
1044) -> Result<()> {
1045 cancel.check()?;
1046 let point_format_id = if has_color { 7u8 } else { 6u8 };
1047 let mut point_format =
1048 LasFormat::new(point_format_id).map_err(|e| Error::Las(format!("point format: {e}")))?;
1049 let extra_byte_count = source.extra_byte_count();
1050 point_format.extra_bytes = extra_byte_count;
1051 let point_record_length = point_format.len();
1052
1053 let (scale_x, scale_y, scale_z) = metadata.scale;
1054 let (offset_x, offset_y, offset_z) =
1055 metadata
1056 .offset
1057 .unwrap_or((bounds.min.0, bounds.min.1, bounds.min.2));
1058 let point_stats = validate_coordinate_inputs(
1059 source,
1060 bounds,
1061 (scale_x, scale_y, scale_z),
1062 (offset_x, offset_y, offset_z),
1063 cancel,
1064 )?;
1065 let (center, halfsize) = cube_from_bounds(&bounds);
1066
1067 let lod_index = build_lod_index(source, center, halfsize, params, cancel)?;
1068 cancel.check()?;
1069
1070 let var_vlr = LazVlrBuilder::default()
1071 .with_point_format(point_format_id, extra_byte_count)
1072 .map_err(|e| Error::Las(format!("laz items: {e}")))?
1073 .with_variable_chunk_size()
1074 .build();
1075 let mut var_vlr_bytes = Vec::new();
1076 var_vlr
1077 .write_to(&mut var_vlr_bytes)
1078 .map_err(|e| Error::Las(format!("variable chunk LAZ VLR: {e}")))?;
1079
1080 let copc_info_vlr_size = 160u16;
1081 let las_header_size = 375u32;
1082 let regular_crs_vlr_count = metadata.regular_crs_vlr_count();
1083 let regular_crs_vlr_bytes = metadata.regular_crs_vlr_bytes()?;
1084 let extra_bytes_vlrs = source.extra_bytes_vlrs();
1085 let extra_bytes_vlr_bytes = regular_las_vlrs_bytes(extra_bytes_vlrs)?;
1086 let pass_through_vlr_bytes = regular_las_vlrs_bytes(&metadata.pass_through_vlrs)?;
1087 let number_of_vlrs = u32::try_from(
1088 2usize
1089 .checked_add(regular_crs_vlr_count)
1090 .and_then(|count| count.checked_add(extra_bytes_vlrs.len()))
1091 .and_then(|count| count.checked_add(metadata.pass_through_vlrs.len()))
1092 .ok_or_else(|| Error::InvalidInput("VLR count overflow".into()))?,
1093 )
1094 .map_err(|_| Error::InvalidInput("VLR count overflow".into()))?;
1095 let number_of_evlrs = u32::try_from(
1096 1usize
1097 .checked_add(metadata.source_evlr_count_after_hierarchy())
1098 .ok_or_else(|| Error::InvalidInput("EVLR count overflow".into()))?,
1099 )
1100 .map_err(|_| Error::InvalidInput("EVLR count overflow".into()))?;
1101 let var_vlr_body_size = u16::try_from(var_vlr_bytes.len())
1102 .map_err(|_| Error::InvalidInput("LAZ VLR byte size exceeds LAS VLR limit".into()))?;
1103 let var_vlr_storage_bytes = LAS_VLR_HEADER_BYTES
1104 .checked_add(u32::from(var_vlr_body_size))
1105 .ok_or_else(|| Error::InvalidInput("LAZ VLR byte size overflow".into()))?;
1106 let total_vlr_bytes = LAS_VLR_HEADER_BYTES
1107 .checked_add(u32::from(copc_info_vlr_size))
1108 .and_then(|total| total.checked_add(var_vlr_storage_bytes))
1109 .and_then(|total| total.checked_add(regular_crs_vlr_bytes))
1110 .and_then(|total| total.checked_add(extra_bytes_vlr_bytes))
1111 .and_then(|total| total.checked_add(pass_through_vlr_bytes))
1112 .ok_or_else(|| Error::InvalidInput("VLR byte size overflow".into()))?;
1113 let offset_to_point_data = las_header_size
1114 .checked_add(total_vlr_bytes)
1115 .ok_or_else(|| Error::InvalidInput("point data offset overflow".into()))?;
1116
1117 let file = File::create(path).map_err(|e| Error::io("create COPC file", e))?;
1118 let mut writer = BufWriter::new(file);
1119
1120 let header = LasHeader {
1121 point_data_format: point_format_id | 0x80,
1122 point_record_length,
1123 offset_to_point_data,
1124 number_of_vlrs,
1125 file_source_id: metadata.file_source_id,
1126 global_encoding: metadata.global_encoding,
1127 guid: metadata.guid,
1128 system_identifier: metadata.system_identifier.clone(),
1129 generating_software: metadata.generating_software.clone(),
1130 creation_day_of_year: metadata.creation_day_of_year,
1131 creation_year: metadata.creation_year,
1132 scale: (scale_x, scale_y, scale_z),
1133 offset: (offset_x, offset_y, offset_z),
1134 bounds,
1135 legacy_point_count: 0,
1136 total_point_count: source.len() as u64,
1137 offset_to_first_evlr: 0,
1138 number_of_evlrs,
1139 extended_return_counts: point_stats.extended_return_counts,
1140 };
1141 header.write(&mut writer)?;
1142
1143 write_vlr_header(&mut writer, "copc", 1, copc_info_vlr_size, "COPC info")?;
1144 let copc_info_payload_start = writer
1145 .stream_position()
1146 .map_err(|e| Error::io("record COPC info payload offset", e))?;
1147 writer
1148 .write_all(&[0u8; 160])
1149 .map_err(|e| Error::io("write COPC info placeholder", e))?;
1150
1151 write_vlr_header(
1152 &mut writer,
1153 LASZIP_VLR_USER_ID,
1154 LASZIP_VLR_RECORD_ID,
1155 var_vlr_body_size,
1156 "http://laszip.org",
1157 )?;
1158 writer
1159 .write_all(&var_vlr_bytes)
1160 .map_err(|e| Error::io("write LAZ VLR", e))?;
1161
1162 for vlr in metadata.regular_crs_vlrs() {
1163 write_las_vlr(&mut writer, vlr)?;
1164 }
1165 for vlr in extra_bytes_vlrs {
1166 write_las_vlr(&mut writer, vlr)?;
1167 }
1168 for vlr in &metadata.pass_through_vlrs {
1169 write_las_vlr(&mut writer, vlr)?;
1170 }
1171
1172 let point_data_actual_start = writer
1173 .stream_position()
1174 .map_err(|e| Error::io("record point data offset", e))?;
1175 if point_data_actual_start as u32 != offset_to_point_data {
1176 return Err(Error::InvalidInput(format!(
1177 "VLR size accounting mismatch: at {point_data_actual_start}, expected {offset_to_point_data}"
1178 )));
1179 }
1180
1181 let mut compressor = LasZipCompressor::new(&mut writer, var_vlr.clone())
1182 .map_err(|e| Error::Las(format!("compressor: {e}")))?;
1183 let mut hierarchy: Vec<Entry> = Vec::with_capacity(lod_index.nodes.len());
1184 let order_path: &Path = lod_index.order_path.as_ref();
1185 let mut index_reader =
1186 BufReader::new(File::open(order_path).map_err(|e| Error::io("open LOD order", e))?);
1187 let mut point_buf = vec![0u8; point_record_length as usize];
1188 let mut chunk_start_file_offset = compressor
1189 .get_mut()
1190 .stream_position()
1191 .map_err(|e| Error::io("record chunk start", e))?;
1192 chunk_start_file_offset += 8;
1193
1194 for node in &lod_index.nodes {
1195 cancel.check()?;
1196 index_reader
1197 .seek(SeekFrom::Start(node.start))
1198 .map_err(|e| Error::io("seek LOD order", e))?;
1199 for point_index in 0..node.count {
1200 if point_index % CANCEL_POLL_STRIDE == 0 {
1201 cancel.check()?;
1202 }
1203 let source_index = index_reader
1204 .read_u32::<LittleEndian>()
1205 .map_err(|e| Error::io("read LOD order", e))?
1206 as usize;
1207 let fields = source.fields(source_index as usize)?;
1208 encode_point_record(
1209 &mut point_buf,
1210 &fields,
1211 (scale_x, scale_y, scale_z),
1212 (offset_x, offset_y, offset_z),
1213 source_index as usize,
1214 &point_format,
1215 )?;
1216 compressor
1217 .compress_one(&point_buf)
1218 .map_err(|e| Error::Las(format!("compress point: {e}")))?;
1219 }
1220 compressor
1221 .finish_current_chunk()
1222 .map_err(|e| Error::Las(format!("finish chunk: {e}")))?;
1223 let after = compressor
1224 .get_mut()
1225 .stream_position()
1226 .map_err(|e| Error::io("record chunk end", e))?;
1227 let byte_size = i32::try_from(after - chunk_start_file_offset)
1228 .map_err(|_| Error::InvalidInput("LAZ chunk exceeds COPC i32 byte size".into()))?;
1229 let point_count = i32::try_from(node.count)
1230 .map_err(|_| Error::InvalidInput("node point count exceeds COPC i32 range".into()))?;
1231 hierarchy.push(Entry {
1232 key: node.key,
1233 offset: chunk_start_file_offset,
1234 byte_size,
1235 point_count,
1236 });
1237 chunk_start_file_offset = after;
1238 }
1239
1240 cancel.check()?;
1241 compressor
1242 .done()
1243 .map_err(|e| Error::Las(format!("finish compressor: {e}")))?;
1244 drop(compressor);
1245
1246 let hierarchy_evlr_start = writer
1247 .stream_position()
1248 .map_err(|e| Error::io("record hierarchy EVLR start", e))?;
1249 let root_hier_offset = hierarchy_evlr_start
1250 .checked_add(LAS_EVLR_HEADER_BYTES)
1251 .ok_or_else(|| Error::InvalidInput("hierarchy EVLR offset overflow".into()))?;
1252 let mut hierarchy_pages = plan_hierarchy_pages(&hierarchy, VoxelKey::root())?;
1253 let hierarchy_end = assign_hierarchy_page_offsets(&mut hierarchy_pages, root_hier_offset)?;
1254 let hierarchy_body_size = hierarchy_end
1255 .checked_sub(root_hier_offset)
1256 .ok_or_else(|| Error::InvalidInput("hierarchy size overflow".into()))?;
1257 write_evlr_header(
1258 &mut writer,
1259 "copc",
1260 1000,
1261 hierarchy_body_size,
1262 "COPC hierarchy",
1263 )?;
1264 let actual_root_hier_offset = writer
1265 .stream_position()
1266 .map_err(|e| Error::io("record root hierarchy offset", e))?;
1267 if actual_root_hier_offset != root_hier_offset {
1268 return Err(Error::InvalidInput(format!(
1269 "hierarchy offset accounting mismatch: at {actual_root_hier_offset}, expected {root_hier_offset}"
1270 )));
1271 }
1272 write_hierarchy_page_tree(&mut writer, &hierarchy_pages)?;
1273 for evlr in metadata.source_evlrs_after_hierarchy() {
1274 write_las_evlr(&mut writer, evlr)?;
1275 }
1276
1277 writer
1278 .seek(SeekFrom::Start(copc_info_payload_start))
1279 .map_err(|e| Error::io("seek COPC info payload", e))?;
1280 let info = CopcInfo {
1281 center,
1282 halfsize,
1283 spacing: halfsize / 128.0,
1284 root_hier_offset,
1285 root_hier_size: hierarchy_pages.byte_size,
1286 gpstime_min: point_stats.gpstime_min,
1287 gpstime_max: point_stats.gpstime_max,
1288 };
1289 writer
1290 .write_all(&info.write_le_bytes())
1291 .map_err(|e| Error::io("patch COPC info", e))?;
1292
1293 writer
1294 .seek(SeekFrom::Start(235))
1295 .map_err(|e| Error::io("seek first EVLR offset", e))?;
1296 writer
1297 .write_u64::<LittleEndian>(hierarchy_evlr_start)
1298 .map_err(|e| Error::io("patch first EVLR offset", e))?;
1299
1300 writer
1301 .flush()
1302 .map_err(|e| Error::io("flush COPC file", e))?;
1303 Ok(())
1304}
1305
1306#[derive(Debug)]
1307struct HierarchyPagePlan {
1308 key: VoxelKey,
1309 items: Vec<HierarchyPageItem>,
1310 offset: u64,
1311 byte_size: u64,
1312}
1313
1314#[derive(Debug)]
1315enum HierarchyPageItem {
1316 Point(Entry),
1317 Child(Box<HierarchyPagePlan>),
1318}
1319
1320fn plan_hierarchy_pages(entries: &[Entry], key: VoxelKey) -> Result<HierarchyPagePlan> {
1321 if entries.is_empty() {
1322 return Err(Error::InvalidInput(
1323 "cannot write empty hierarchy page".into(),
1324 ));
1325 }
1326 if entries.len() <= HIERARCHY_PAGE_MAX_ENTRIES {
1327 return Ok(HierarchyPagePlan {
1328 key,
1329 items: entries
1330 .iter()
1331 .copied()
1332 .map(HierarchyPageItem::Point)
1333 .collect(),
1334 offset: 0,
1335 byte_size: 0,
1336 });
1337 }
1338
1339 let mut point_entry = None;
1340 let mut child_entries: [Vec<Entry>; 8] = std::array::from_fn(|_| Vec::new());
1341 for entry in entries.iter().copied() {
1342 if entry.key == key {
1343 point_entry = Some(entry);
1344 continue;
1345 }
1346 let mut matched = false;
1347 for (octant, child_entries) in child_entries.iter_mut().enumerate() {
1348 let child_key = key.child(octant as u8);
1349 if key_contains(child_key, entry.key) {
1350 child_entries.push(entry);
1351 matched = true;
1352 break;
1353 }
1354 }
1355 if !matched {
1356 return Err(Error::InvalidInput(format!(
1357 "hierarchy entry {:?} is not under page key {:?}",
1358 entry.key, key
1359 )));
1360 }
1361 }
1362
1363 let mut items = Vec::new();
1364 if let Some(entry) = point_entry {
1365 items.push(HierarchyPageItem::Point(entry));
1366 }
1367 for (octant, child_entries) in child_entries.into_iter().enumerate() {
1368 if child_entries.is_empty() {
1369 continue;
1370 }
1371 items.push(HierarchyPageItem::Child(Box::new(plan_hierarchy_pages(
1372 &child_entries,
1373 key.child(octant as u8),
1374 )?)));
1375 }
1376 if items.len() > HIERARCHY_PAGE_MAX_ENTRIES {
1377 return Err(Error::InvalidInput(format!(
1378 "hierarchy page for {:?} has {} entries, max is {}",
1379 key,
1380 items.len(),
1381 HIERARCHY_PAGE_MAX_ENTRIES
1382 )));
1383 }
1384 Ok(HierarchyPagePlan {
1385 key,
1386 items,
1387 offset: 0,
1388 byte_size: 0,
1389 })
1390}
1391
1392fn assign_hierarchy_page_offsets(page: &mut HierarchyPagePlan, offset: u64) -> Result<u64> {
1393 page.offset = offset;
1394 page.byte_size = hierarchy_page_byte_size(page.items.len())?;
1395 let mut next = offset
1396 .checked_add(page.byte_size)
1397 .ok_or_else(|| Error::InvalidInput("hierarchy page offset overflow".into()))?;
1398 for item in &mut page.items {
1399 if let HierarchyPageItem::Child(child) = item {
1400 next = assign_hierarchy_page_offsets(child, next)?;
1401 }
1402 }
1403 Ok(next)
1404}
1405
1406fn hierarchy_page_byte_size(entry_count: usize) -> Result<u64> {
1407 let bytes = entry_count
1408 .checked_mul(HIERARCHY_ENTRY_BYTES)
1409 .ok_or_else(|| Error::InvalidInput("hierarchy page size overflow".into()))?;
1410 u64::try_from(bytes).map_err(|_| Error::InvalidInput("hierarchy page is too large".into()))
1411}
1412
1413fn write_hierarchy_page_tree<W: Write + Seek>(
1414 writer: &mut W,
1415 page: &HierarchyPagePlan,
1416) -> Result<()> {
1417 let position = writer
1418 .stream_position()
1419 .map_err(|e| Error::io("record hierarchy page offset", e))?;
1420 if position != page.offset {
1421 return Err(Error::InvalidInput(format!(
1422 "hierarchy page offset mismatch: at {position}, expected {}",
1423 page.offset
1424 )));
1425 }
1426 let mut entry_buf = [0u8; HIERARCHY_ENTRY_BYTES];
1427 for item in &page.items {
1428 hierarchy_page_item_entry(item)?.write_le(&mut entry_buf)?;
1429 writer
1430 .write_all(&entry_buf)
1431 .map_err(|e| Error::io("write hierarchy entry", e))?;
1432 }
1433 for item in &page.items {
1434 if let HierarchyPageItem::Child(child) = item {
1435 write_hierarchy_page_tree(writer, child)?;
1436 }
1437 }
1438 Ok(())
1439}
1440
1441fn hierarchy_page_item_entry(item: &HierarchyPageItem) -> Result<Entry> {
1442 match item {
1443 HierarchyPageItem::Point(entry) => Ok(*entry),
1444 HierarchyPageItem::Child(child) => Ok(Entry {
1445 key: child.key,
1446 offset: child.offset,
1447 byte_size: i32::try_from(child.byte_size).map_err(|_| {
1448 Error::InvalidInput("child hierarchy page exceeds COPC i32 byte size".into())
1449 })?,
1450 point_count: -1,
1451 }),
1452 }
1453}
1454
1455fn key_contains(ancestor: VoxelKey, key: VoxelKey) -> bool {
1456 if key.level < ancestor.level {
1457 return false;
1458 }
1459 let shift = (key.level - ancestor.level) as u32;
1460 (key.x >> shift) == ancestor.x
1461 && (key.y >> shift) == ancestor.y
1462 && (key.z >> shift) == ancestor.z
1463}
1464
1465struct LodIndex {
1466 nodes: Vec<LodNodeRange>,
1467 order_path: TempPath,
1468}
1469
1470#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1471struct LodNodeRange {
1472 key: VoxelKey,
1473 start: u64,
1474 count: usize,
1475}
1476
1477struct IndexRun {
1478 path: TempPath,
1479 start: u64,
1480 count: usize,
1481}
1482
1483fn build_lod_index<S: CopcPointSource>(
1484 source: &S,
1485 center: (f64, f64, f64),
1486 halfsize: f64,
1487 params: &CopcWriterParams,
1488 cancel: &dyn CancelCheck,
1489) -> Result<LodIndex> {
1490 cancel.check()?;
1491 let total_points = u32::try_from(source.len()).map_err(|_| {
1492 Error::InvalidInput("COPC writer supports at most u32::MAX points per file".into())
1493 })?;
1494 let max_points_per_node = params.max_points_per_node.max(1) as usize;
1495 let max_depth = params.max_depth.clamp(MIN_LEAF_DEPTH, MAX_LEAF_DEPTH);
1496 let root_run = write_root_index_run(total_points, cancel)?;
1497 let mut order_file = new_index_tempfile("order")?;
1498 let mut order_offset = 0;
1499 let mut nodes = Vec::new();
1500 {
1501 let mut order_writer = BufWriter::new(order_file.as_file_mut());
1502 let mut builder = LodIndexBuilder {
1503 source,
1504 max_points_per_node,
1505 max_depth,
1506 cancel,
1507 order_writer: &mut order_writer,
1508 order_offset: &mut order_offset,
1509 nodes: &mut nodes,
1510 };
1511 builder.assign(VoxelKey::root(), root_run, Bounds::cube(center, halfsize))?;
1512 order_writer
1513 .flush()
1514 .map_err(|e| Error::io("flush LOD index order", e))?;
1515 }
1516 nodes.sort_by_key(|node| node.key);
1517 Ok(LodIndex {
1518 nodes,
1519 order_path: order_file.into_temp_path(),
1520 })
1521}
1522
1523struct LodIndexBuilder<'a, S: CopcPointSource, W: Write> {
1524 source: &'a S,
1525 max_points_per_node: usize,
1526 max_depth: u32,
1527 cancel: &'a dyn CancelCheck,
1528 order_writer: &'a mut W,
1529 order_offset: &'a mut u64,
1530 nodes: &'a mut Vec<LodNodeRange>,
1531}
1532
1533impl<S: CopcPointSource, W: Write> LodIndexBuilder<'_, S, W> {
1534 fn assign(&mut self, key: VoxelKey, run: IndexRun, bounds: Bounds) -> Result<()> {
1535 self.cancel.check()?;
1536 if run.count == 0 {
1537 return Ok(());
1538 }
1539 if run.count <= self.max_points_per_node || key.level as u32 >= self.max_depth {
1540 let start = *self.order_offset;
1541 append_index_run_to_order(&run, self.order_writer, self.order_offset, self.cancel)?;
1542 self.nodes.push(LodNodeRange {
1543 key,
1544 start,
1545 count: run.count,
1546 });
1547 return Ok(());
1548 }
1549
1550 let mut children = partition_index_run(self.source, &run, bounds, self.cancel)?;
1551 let start = *self.order_offset;
1552 let selected_counts = append_lod_selection_to_order(
1553 &children,
1554 self.max_points_per_node,
1555 self.order_writer,
1556 self.order_offset,
1557 self.cancel,
1558 )?;
1559 let selected_total = selected_counts.iter().sum();
1560 self.nodes.push(LodNodeRange {
1561 key,
1562 start,
1563 count: selected_total,
1564 });
1565
1566 for (octant, child) in children.iter_mut().enumerate() {
1567 let Some(mut child_run) = child.take() else {
1568 continue;
1569 };
1570 let selected = selected_counts[octant];
1571 if selected >= child_run.count {
1572 continue;
1573 }
1574 child_run.start += selected as u64 * INDEX_RECORD_BYTES;
1575 child_run.count -= selected;
1576 self.assign(
1577 key.child(octant as u8),
1578 child_run,
1579 bounds.octant(octant as u8),
1580 )?;
1581 }
1582 Ok(())
1583 }
1584}
1585
1586fn write_root_index_run(total_points: u32, cancel: &dyn CancelCheck) -> Result<IndexRun> {
1587 let mut writer = BufWriter::new(new_index_tempfile("root")?);
1588 for index in 0..total_points {
1589 if index as usize % CANCEL_POLL_STRIDE == 0 {
1590 cancel.check()?;
1591 }
1592 writer
1593 .write_u32::<LittleEndian>(index)
1594 .map_err(|e| Error::io("write root LOD index", e))?;
1595 }
1596 let file = writer
1597 .into_inner()
1598 .map_err(|e| Error::io("flush root LOD index", e.into_error()))?;
1599 Ok(IndexRun {
1600 path: file.into_temp_path(),
1601 start: 0,
1602 count: total_points as usize,
1603 })
1604}
1605
1606fn partition_index_run<S: CopcPointSource>(
1607 source: &S,
1608 run: &IndexRun,
1609 bounds: Bounds,
1610 cancel: &dyn CancelCheck,
1611) -> Result<[Option<IndexRun>; 8]> {
1612 let mut reader = open_index_run(run)?;
1613 let mut writers: [Option<BufWriter<NamedTempFile>>; 8] = std::array::from_fn(|_| None);
1614 let mut counts = [0usize; 8];
1615 for read_index in 0..run.count {
1616 if read_index % CANCEL_POLL_STRIDE == 0 {
1617 cancel.check()?;
1618 }
1619 let index = reader
1620 .read_u32::<LittleEndian>()
1621 .map_err(|e| Error::io("read LOD partition index", e))?;
1622 let (x, y, z) = source.xyz(index as usize);
1623 let octant = child_octant(bounds, x, y, z);
1624 if writers[octant].is_none() {
1625 writers[octant] = Some(BufWriter::new(new_index_tempfile("partition")?));
1626 }
1627 writers[octant]
1628 .as_mut()
1629 .expect("partition writer exists")
1630 .write_u32::<LittleEndian>(index)
1631 .map_err(|e| Error::io("write LOD partition index", e))?;
1632 counts[octant] += 1;
1633 }
1634
1635 let mut children: [Option<IndexRun>; 8] = std::array::from_fn(|_| None);
1636 for octant in 0..8 {
1637 let Some(writer) = writers[octant].take() else {
1638 continue;
1639 };
1640 let file = writer
1641 .into_inner()
1642 .map_err(|e| Error::io("flush LOD partition index", e.into_error()))?;
1643 children[octant] = Some(IndexRun {
1644 path: file.into_temp_path(),
1645 start: 0,
1646 count: counts[octant],
1647 });
1648 }
1649 Ok(children)
1650}
1651
1652fn append_lod_selection_to_order<W: Write>(
1653 children: &[Option<IndexRun>; 8],
1654 max_points_per_node: usize,
1655 order_writer: &mut W,
1656 order_offset: &mut u64,
1657 cancel: &dyn CancelCheck,
1658) -> Result<[usize; 8]> {
1659 let mut readers: [Option<BufReader<File>>; 8] = std::array::from_fn(|_| None);
1660 for octant in 0..8 {
1661 if let Some(child) = &children[octant] {
1662 readers[octant] = Some(open_index_run(child)?);
1663 }
1664 }
1665
1666 let mut selected_counts = [0usize; 8];
1667 let mut selected_total = 0usize;
1668 while selected_total < max_points_per_node {
1669 cancel.check()?;
1670 let mut progressed = false;
1671 for octant in 0..8 {
1672 let Some(child) = &children[octant] else {
1673 continue;
1674 };
1675 if selected_counts[octant] >= child.count {
1676 continue;
1677 }
1678 let index = readers[octant]
1679 .as_mut()
1680 .expect("partition reader exists")
1681 .read_u32::<LittleEndian>()
1682 .map_err(|e| Error::io("read selected LOD index", e))?;
1683 append_index_to_order(order_writer, order_offset, index)?;
1684 selected_counts[octant] += 1;
1685 selected_total += 1;
1686 progressed = true;
1687 if selected_total == max_points_per_node {
1688 break;
1689 }
1690 }
1691 if !progressed {
1692 break;
1693 }
1694 }
1695 Ok(selected_counts)
1696}
1697
1698fn append_index_run_to_order<W: Write>(
1699 run: &IndexRun,
1700 order_writer: &mut W,
1701 order_offset: &mut u64,
1702 cancel: &dyn CancelCheck,
1703) -> Result<()> {
1704 let mut reader = open_index_run(run)?;
1705 for read_index in 0..run.count {
1706 if read_index % CANCEL_POLL_STRIDE == 0 {
1707 cancel.check()?;
1708 }
1709 let index = reader
1710 .read_u32::<LittleEndian>()
1711 .map_err(|e| Error::io("read LOD index", e))?;
1712 append_index_to_order(order_writer, order_offset, index)?;
1713 }
1714 Ok(())
1715}
1716
1717fn append_index_to_order<W: Write>(
1718 order_writer: &mut W,
1719 order_offset: &mut u64,
1720 index: u32,
1721) -> Result<()> {
1722 order_writer
1723 .write_u32::<LittleEndian>(index)
1724 .map_err(|e| Error::io("write LOD index order", e))?;
1725 *order_offset = order_offset
1726 .checked_add(INDEX_RECORD_BYTES)
1727 .ok_or_else(|| Error::InvalidInput("LOD index order exceeds u64 range".into()))?;
1728 Ok(())
1729}
1730
1731fn open_index_run(run: &IndexRun) -> Result<BufReader<File>> {
1732 let path: &Path = run.path.as_ref();
1733 let mut file = File::open(path).map_err(|e| Error::io("open LOD index", e))?;
1734 file.seek(SeekFrom::Start(run.start))
1735 .map_err(|e| Error::io("seek LOD index", e))?;
1736 Ok(BufReader::new(file))
1737}
1738
1739fn new_index_tempfile(label: &str) -> Result<NamedTempFile> {
1740 let prefix = format!(".copc-writer-{label}.");
1741 tempfile::Builder::new()
1742 .prefix(&prefix)
1743 .suffix(".idx")
1744 .tempfile()
1745 .map_err(|e| Error::io("create LOD index file", e))
1746}
1747
1748fn child_octant(bounds: Bounds, x: f64, y: f64, z: f64) -> usize {
1749 let center = bounds.center();
1750 usize::from(x >= center.0)
1751 | (usize::from(y >= center.1) << 1)
1752 | (usize::from(z >= center.2) << 2)
1753}
1754
1755fn cube_from_bounds(bounds: &Bounds) -> ((f64, f64, f64), f64) {
1756 let dx = bounds.max.0 - bounds.min.0;
1757 let dy = bounds.max.1 - bounds.min.1;
1758 let dz = bounds.max.2 - bounds.min.2;
1759 let center = (
1760 bounds.min.0 + dx * 0.5,
1761 bounds.min.1 + dy * 0.5,
1762 bounds.min.2 + dz * 0.5,
1763 );
1764 let halfsize = (dx.max(dy).max(dz) * 0.5).max(1e-6);
1765 (center, halfsize)
1766}
1767
1768struct LasHeader {
1769 point_data_format: u8,
1770 point_record_length: u16,
1771 offset_to_point_data: u32,
1772 number_of_vlrs: u32,
1773 file_source_id: u16,
1774 global_encoding: u16,
1775 guid: [u8; 16],
1776 system_identifier: String,
1777 generating_software: String,
1778 creation_day_of_year: u16,
1779 creation_year: u16,
1780 scale: (f64, f64, f64),
1781 offset: (f64, f64, f64),
1782 bounds: Bounds,
1783 legacy_point_count: u32,
1784 total_point_count: u64,
1785 offset_to_first_evlr: u64,
1786 number_of_evlrs: u32,
1787 extended_return_counts: [u64; 15],
1788}
1789
1790impl LasHeader {
1791 fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
1792 writer
1793 .write_all(b"LASF")
1794 .map_err(|e| Error::io("write LAS signature", e))?;
1795 writer
1796 .write_u16::<LittleEndian>(self.file_source_id)
1797 .map_err(|e| Error::io("write file source id", e))?;
1798 writer
1799 .write_u16::<LittleEndian>(self.global_encoding)
1800 .map_err(|e| Error::io("write global encoding", e))?;
1801 writer
1802 .write_all(&self.guid)
1803 .map_err(|e| Error::io("write GUID", e))?;
1804 writer
1805 .write_u8(1)
1806 .map_err(|e| Error::io("write version major", e))?;
1807 writer
1808 .write_u8(4)
1809 .map_err(|e| Error::io("write version minor", e))?;
1810 writer
1811 .write_all(&pad(self.system_identifier.as_bytes(), 32))
1812 .map_err(|e| Error::io("write system id", e))?;
1813 writer
1814 .write_all(&pad(self.generating_software.as_bytes(), 32))
1815 .map_err(|e| Error::io("write generating software", e))?;
1816 writer
1817 .write_u16::<LittleEndian>(self.creation_day_of_year)
1818 .map_err(|e| Error::io("write creation day", e))?;
1819 writer
1820 .write_u16::<LittleEndian>(self.creation_year)
1821 .map_err(|e| Error::io("write creation year", e))?;
1822 writer
1823 .write_u16::<LittleEndian>(375)
1824 .map_err(|e| Error::io("write header size", e))?;
1825 writer
1826 .write_u32::<LittleEndian>(self.offset_to_point_data)
1827 .map_err(|e| Error::io("write point data offset", e))?;
1828 writer
1829 .write_u32::<LittleEndian>(self.number_of_vlrs)
1830 .map_err(|e| Error::io("write VLR count", e))?;
1831 writer
1832 .write_u8(self.point_data_format)
1833 .map_err(|e| Error::io("write point format", e))?;
1834 writer
1835 .write_u16::<LittleEndian>(self.point_record_length)
1836 .map_err(|e| Error::io("write point record length", e))?;
1837 writer
1838 .write_u32::<LittleEndian>(self.legacy_point_count)
1839 .map_err(|e| Error::io("write legacy point count", e))?;
1840 for _ in 0..5 {
1841 writer
1842 .write_u32::<LittleEndian>(0)
1843 .map_err(|e| Error::io("write legacy returns", e))?;
1844 }
1845 writer
1846 .write_f64::<LittleEndian>(self.scale.0)
1847 .map_err(|e| Error::io("write x scale", e))?;
1848 writer
1849 .write_f64::<LittleEndian>(self.scale.1)
1850 .map_err(|e| Error::io("write y scale", e))?;
1851 writer
1852 .write_f64::<LittleEndian>(self.scale.2)
1853 .map_err(|e| Error::io("write z scale", e))?;
1854 writer
1855 .write_f64::<LittleEndian>(self.offset.0)
1856 .map_err(|e| Error::io("write x offset", e))?;
1857 writer
1858 .write_f64::<LittleEndian>(self.offset.1)
1859 .map_err(|e| Error::io("write y offset", e))?;
1860 writer
1861 .write_f64::<LittleEndian>(self.offset.2)
1862 .map_err(|e| Error::io("write z offset", e))?;
1863 writer
1864 .write_f64::<LittleEndian>(self.bounds.max.0)
1865 .map_err(|e| Error::io("write max x", e))?;
1866 writer
1867 .write_f64::<LittleEndian>(self.bounds.min.0)
1868 .map_err(|e| Error::io("write min x", e))?;
1869 writer
1870 .write_f64::<LittleEndian>(self.bounds.max.1)
1871 .map_err(|e| Error::io("write max y", e))?;
1872 writer
1873 .write_f64::<LittleEndian>(self.bounds.min.1)
1874 .map_err(|e| Error::io("write min y", e))?;
1875 writer
1876 .write_f64::<LittleEndian>(self.bounds.max.2)
1877 .map_err(|e| Error::io("write max z", e))?;
1878 writer
1879 .write_f64::<LittleEndian>(self.bounds.min.2)
1880 .map_err(|e| Error::io("write min z", e))?;
1881 writer
1882 .write_u64::<LittleEndian>(0)
1883 .map_err(|e| Error::io("write waveform packet start", e))?;
1884 writer
1885 .write_u64::<LittleEndian>(self.offset_to_first_evlr)
1886 .map_err(|e| Error::io("write first EVLR offset", e))?;
1887 writer
1888 .write_u32::<LittleEndian>(self.number_of_evlrs)
1889 .map_err(|e| Error::io("write EVLR count", e))?;
1890 writer
1891 .write_u64::<LittleEndian>(self.total_point_count)
1892 .map_err(|e| Error::io("write total point count", e))?;
1893 for count in self.extended_return_counts {
1894 writer
1895 .write_u64::<LittleEndian>(count)
1896 .map_err(|e| Error::io("write extended returns", e))?;
1897 }
1898 Ok(())
1899 }
1900}
1901
1902fn pad(value: &[u8], len: usize) -> Vec<u8> {
1903 let mut out = Vec::with_capacity(len);
1904 let take = value.len().min(len);
1905 out.extend_from_slice(&value[..take]);
1906 out.resize(len, 0);
1907 out
1908}
1909
1910fn write_vlr_header<W: Write>(
1911 writer: &mut W,
1912 user_id: &str,
1913 record_id: u16,
1914 body_size: u16,
1915 description: &str,
1916) -> Result<()> {
1917 writer
1918 .write_u16::<LittleEndian>(0)
1919 .map_err(|e| Error::io("write VLR reserved", e))?;
1920 writer
1921 .write_all(&pad(user_id.as_bytes(), 16))
1922 .map_err(|e| Error::io("write VLR user id", e))?;
1923 writer
1924 .write_u16::<LittleEndian>(record_id)
1925 .map_err(|e| Error::io("write VLR record id", e))?;
1926 writer
1927 .write_u16::<LittleEndian>(body_size)
1928 .map_err(|e| Error::io("write VLR body size", e))?;
1929 writer
1930 .write_all(&pad(description.as_bytes(), 32))
1931 .map_err(|e| Error::io("write VLR description", e))?;
1932 Ok(())
1933}
1934
1935fn write_las_vlr<W: Write>(writer: &mut W, vlr: &las::Vlr) -> Result<()> {
1936 let body_size = u16::try_from(vlr.data.len()).map_err(|_| {
1937 Error::InvalidInput(format!(
1938 "regular VLR {}:{} is too large: {} byte(s)",
1939 vlr.user_id,
1940 vlr.record_id,
1941 vlr.data.len()
1942 ))
1943 })?;
1944 write_vlr_header(
1945 writer,
1946 &vlr.user_id,
1947 vlr.record_id,
1948 body_size,
1949 &vlr.description,
1950 )?;
1951 writer
1952 .write_all(&vlr.data)
1953 .map_err(|e| Error::io("write VLR body", e))?;
1954 Ok(())
1955}
1956
1957fn regular_las_vlrs_bytes(vlrs: &[las::Vlr]) -> Result<u32> {
1958 vlrs.iter().try_fold(0u32, |total, vlr| {
1959 let data_len = u16::try_from(vlr.data.len()).map_err(|_| {
1960 Error::InvalidInput(format!(
1961 "regular VLR {}:{} is too large: {} byte(s)",
1962 vlr.user_id,
1963 vlr.record_id,
1964 vlr.data.len()
1965 ))
1966 })?;
1967 total
1968 .checked_add(LAS_VLR_HEADER_BYTES + u32::from(data_len))
1969 .ok_or_else(|| Error::InvalidInput("VLR byte size overflow".into()))
1970 })
1971}
1972
1973fn write_evlr_header<W: Write>(
1974 writer: &mut W,
1975 user_id: &str,
1976 record_id: u16,
1977 body_size: u64,
1978 description: &str,
1979) -> Result<()> {
1980 writer
1981 .write_u16::<LittleEndian>(0)
1982 .map_err(|e| Error::io("write EVLR reserved", e))?;
1983 writer
1984 .write_all(&pad(user_id.as_bytes(), 16))
1985 .map_err(|e| Error::io("write EVLR user id", e))?;
1986 writer
1987 .write_u16::<LittleEndian>(record_id)
1988 .map_err(|e| Error::io("write EVLR record id", e))?;
1989 writer
1990 .write_u64::<LittleEndian>(body_size)
1991 .map_err(|e| Error::io("write EVLR body size", e))?;
1992 writer
1993 .write_all(&pad(description.as_bytes(), 32))
1994 .map_err(|e| Error::io("write EVLR description", e))?;
1995 Ok(())
1996}
1997
1998fn write_las_evlr<W: Write>(writer: &mut W, vlr: &las::Vlr) -> Result<()> {
1999 let body_size = u64::try_from(vlr.data.len()).map_err(|_| {
2000 Error::InvalidInput(format!(
2001 "EVLR {}:{} is too large: {} byte(s)",
2002 vlr.user_id,
2003 vlr.record_id,
2004 vlr.data.len()
2005 ))
2006 })?;
2007 write_evlr_header(
2008 writer,
2009 &vlr.user_id,
2010 vlr.record_id,
2011 body_size,
2012 &vlr.description,
2013 )?;
2014 writer
2015 .write_all(&vlr.data)
2016 .map_err(|e| Error::io("write EVLR body", e))?;
2017 Ok(())
2018}
2019
2020fn encode_point_record(
2021 buf: &mut [u8],
2022 fields: &CopcPointFields,
2023 scale: (f64, f64, f64),
2024 offset: (f64, f64, f64),
2025 point_index: usize,
2026 format: &LasFormat,
2027) -> Result<()> {
2028 let mut cursor = Cursor::new(buf);
2029 let (ix, iy, iz) = quantize_xyz(point_index, fields.x, fields.y, fields.z, scale, offset)?;
2030 let flags =
2031 fields.synthetic | (fields.key_point << 1) | (fields.withheld << 2) | (fields.overlap << 3);
2032 let point = raw::Point {
2033 x: ix,
2034 y: iy,
2035 z: iz,
2036 intensity: fields.intensity,
2037 flags: raw::point::Flags::ThreeByte(
2038 fields.return_number | (fields.number_of_returns << 4),
2039 flags
2040 | (fields.scan_channel << 4)
2041 | (fields.scan_direction_flag << 6)
2042 | (fields.edge_of_flight_line << 7),
2043 fields.classification,
2044 ),
2045 scan_angle: raw::point::ScanAngle::from(fields.scan_angle),
2046 user_data: fields.user_data,
2047 point_source_id: fields.point_source_id,
2048 gps_time: Some(fields.gps_time),
2049 color: format
2050 .has_color
2051 .then_some(Color::new(fields.red, fields.green, fields.blue)),
2052 waveform: None,
2053 nir: None,
2054 extra_bytes: fields.extra_bytes.clone(),
2055 };
2056 point
2057 .write_to(&mut cursor, format)
2058 .map_err(|e| Error::Las(format!("write point record: {e}")))?;
2059 Ok(())
2060}
2061
2062#[cfg(test)]
2063mod tests {
2064 use super::*;
2065
2066 struct VecSource {
2067 points: Vec<CopcPointFields>,
2068 }
2069
2070 impl CopcPointSource for VecSource {
2071 fn len(&self) -> usize {
2072 self.points.len()
2073 }
2074
2075 fn xyz(&self, index: usize) -> (f64, f64, f64) {
2076 let point = &self.points[index];
2077 (point.x, point.y, point.z)
2078 }
2079
2080 fn fields(&self, index: usize) -> Result<CopcPointFields> {
2081 Ok(self.points[index].clone())
2082 }
2083 }
2084
2085 #[test]
2086 fn spooled_lod_index_covers_each_point_once() {
2087 let points = (0..257)
2088 .map(|i| CopcPointFields {
2089 x: f64::from((i * 37) % 101),
2090 y: f64::from((i * 53) % 103),
2091 z: f64::from((i * 71) % 107),
2092 intensity: 0,
2093 return_number: 1,
2094 number_of_returns: 1,
2095 synthetic: 0,
2096 key_point: 0,
2097 withheld: 0,
2098 overlap: 0,
2099 scan_channel: 0,
2100 scan_direction_flag: 0,
2101 edge_of_flight_line: 0,
2102 classification: 0,
2103 user_data: 0,
2104 scan_angle: 0.0,
2105 point_source_id: 0,
2106 gps_time: f64::from(i),
2107 red: 0,
2108 green: 0,
2109 blue: 0,
2110 extra_bytes: Vec::new(),
2111 })
2112 .collect();
2113 let source = VecSource { points };
2114 let bounds = source_bounds(&source);
2115 let (center, halfsize) = cube_from_bounds(&bounds);
2116 let params = CopcWriterParams {
2117 max_points_per_node: 7,
2118 max_depth: 5,
2119 };
2120
2121 let spooled = build_lod_index(&source, center, halfsize, ¶ms, &NeverCancel).unwrap();
2122 let ranges = read_lod_index(&spooled).unwrap();
2123
2124 let mut seen = vec![false; source.len()];
2125 let mut total = 0usize;
2126 for (key, indices) in ranges {
2127 if key.level < params.max_depth as i32 {
2128 assert!(indices.len() <= params.max_points_per_node as usize);
2129 }
2130 for index in indices {
2131 let seen = &mut seen[index as usize];
2132 assert!(!*seen, "point index {index} was assigned more than once");
2133 *seen = true;
2134 total += 1;
2135 }
2136 }
2137 assert_eq!(source.len(), total);
2138 assert!(seen.into_iter().all(|value| value));
2139 }
2140
2141 #[test]
2142 fn dense_cluster_stays_bounded_below_giant_chunks() {
2143 let field = |x: f64, y: f64, z: f64, i: u32| CopcPointFields {
2149 x,
2150 y,
2151 z,
2152 intensity: 0,
2153 return_number: 1,
2154 number_of_returns: 1,
2155 synthetic: 0,
2156 key_point: 0,
2157 withheld: 0,
2158 overlap: 0,
2159 scan_channel: 0,
2160 scan_direction_flag: 0,
2161 edge_of_flight_line: 0,
2162 classification: 0,
2163 user_data: 0,
2164 scan_angle: 0.0,
2165 point_source_id: 0,
2166 gps_time: f64::from(i),
2167 red: 0,
2168 green: 0,
2169 blue: 0,
2170 extra_bytes: Vec::new(),
2171 };
2172 let mut points: Vec<CopcPointFields> = (0..4_000u32)
2174 .map(|i| {
2175 let f = f64::from(i);
2176 field(
2177 f * 1e-4,
2178 (f * 1.7).fract() * 0.4,
2179 (f * 2.3).fract() * 0.4,
2180 i,
2181 )
2182 })
2183 .collect();
2184 for i in 0..8u32 {
2186 points.push(field(
2187 f64::from(i) * 1000.0,
2188 f64::from(i) * 1000.0,
2189 f64::from(i) * 100.0,
2190 100_000 + i,
2191 ));
2192 }
2193 let max_points = 100usize;
2194 let source = VecSource { points };
2195 let bounds = source_bounds(&source);
2196 let (center, halfsize) = cube_from_bounds(&bounds);
2197 let params = CopcWriterParams {
2199 max_points_per_node: max_points as u32,
2200 max_depth: 3,
2201 };
2202
2203 let lod = build_lod_index(&source, center, halfsize, ¶ms, &NeverCancel).unwrap();
2204 for (key, indices) in read_lod_index(&lod).unwrap() {
2205 assert!(
2206 indices.len() <= max_points,
2207 "node {key:?} holds {} points, exceeding max_points_per_node {max_points}",
2208 indices.len(),
2209 );
2210 }
2211 }
2212
2213 #[test]
2214 fn hierarchy_plan_splits_large_root_page() {
2215 let mut entries = vec![Entry {
2216 key: VoxelKey::root(),
2217 offset: 1,
2218 byte_size: 1,
2219 point_count: 1,
2220 }];
2221 let mut offset = 2;
2222 for z in 0..16 {
2223 for y in 0..16 {
2224 for x in 0..16 {
2225 entries.push(Entry {
2226 key: VoxelKey { level: 4, x, y, z },
2227 offset,
2228 byte_size: 1,
2229 point_count: 1,
2230 });
2231 offset += 1;
2232 }
2233 }
2234 }
2235 entries.sort_by_key(|entry| entry.key);
2236
2237 let mut plan = plan_hierarchy_pages(&entries, VoxelKey::root()).unwrap();
2238 let start = 1024;
2239 let end = assign_hierarchy_page_offsets(&mut plan, start).unwrap();
2240
2241 assert!(plan.byte_size < hierarchy_page_byte_size(entries.len()).unwrap());
2242 assert!(plan
2243 .items
2244 .iter()
2245 .any(|item| matches!(item, HierarchyPageItem::Child(_))));
2246
2247 let mut out = Cursor::new(vec![0; start as usize]);
2248 out.seek(SeekFrom::Start(start)).unwrap();
2249 write_hierarchy_page_tree(&mut out, &plan).unwrap();
2250 assert_eq!(end, out.get_ref().len() as u64);
2251 }
2252
2253 fn source_bounds(source: &VecSource) -> Bounds {
2254 source.points.iter().fold(
2255 Bounds::point(source.points[0].x, source.points[0].y, source.points[0].z),
2256 |mut bounds, point| {
2257 bounds.extend(point.x, point.y, point.z);
2258 bounds
2259 },
2260 )
2261 }
2262
2263 fn read_lod_index(index: &LodIndex) -> Result<Vec<(VoxelKey, Vec<u32>)>> {
2264 let path: &Path = index.order_path.as_ref();
2265 let mut reader =
2266 BufReader::new(File::open(path).map_err(|e| Error::io("open LOD order", e))?);
2267 let mut out = Vec::new();
2268 for node in &index.nodes {
2269 reader
2270 .seek(SeekFrom::Start(node.start))
2271 .map_err(|e| Error::io("seek LOD order", e))?;
2272 let mut indices = Vec::with_capacity(node.count);
2273 for _ in 0..node.count {
2274 indices.push(
2275 reader
2276 .read_u32::<LittleEndian>()
2277 .map_err(|e| Error::io("read LOD order", e))?,
2278 );
2279 }
2280 out.push((node.key, indices));
2281 }
2282 Ok(out)
2283 }
2284}