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