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, CopcInfo, Entry, Error, LasPointRecord, NeverCancel, Result,
8 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 LAS_14_SCAN_ANGLE_SCALE: f32 = 0.006;
20const LASZIP_VLR_USER_ID: &str = "laszip encoded";
21const LASZIP_VLR_RECORD_ID: u16 = 22204;
22
23#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct CopcPointFields {
26 pub x: f64,
27 pub y: f64,
28 pub z: f64,
29 pub intensity: u16,
30 pub return_number: u8,
31 pub number_of_returns: u8,
32 pub synthetic: u8,
33 pub key_point: u8,
34 pub withheld: u8,
35 pub overlap: u8,
36 pub scan_channel: u8,
37 pub scan_direction_flag: u8,
38 pub edge_of_flight_line: u8,
39 pub classification: u8,
40 pub user_data: u8,
41 pub scan_angle: f32,
43 pub point_source_id: u16,
44 pub gps_time: f64,
45 pub red: u16,
46 pub green: u16,
47 pub blue: u16,
48}
49
50pub trait CopcPointSource {
52 fn len(&self) -> usize;
53 fn xyz(&self, index: usize) -> (f64, f64, f64);
54 fn fields(&self, index: usize) -> Result<CopcPointFields>;
55
56 fn is_empty(&self) -> bool {
57 self.len() == 0
58 }
59}
60
61struct SpillSource<'a> {
62 reader: &'a SpillReader,
63}
64
65impl CopcPointSource for SpillSource<'_> {
66 fn len(&self) -> usize {
67 self.reader.len()
68 }
69
70 #[inline]
71 fn xyz(&self, index: usize) -> (f64, f64, f64) {
72 self.reader.xyz_at(index)
73 }
74
75 fn fields(&self, index: usize) -> Result<CopcPointFields> {
76 let record = self.reader.record_at(index)?;
77 Ok(CopcPointFields {
78 x: record.x,
79 y: record.y,
80 z: record.z,
81 intensity: record.intensity,
82 return_number: record.return_number,
83 number_of_returns: record.number_of_returns,
84 synthetic: u8::from(record.synthetic),
85 key_point: u8::from(record.key_point),
86 withheld: u8::from(record.withheld),
87 overlap: u8::from(record.overlap),
88 scan_channel: record.scan_channel,
89 scan_direction_flag: u8::from(record.scan_direction_flag),
90 edge_of_flight_line: u8::from(record.edge_of_flight_line),
91 classification: record.classification,
92 user_data: record.user_data,
93 scan_angle: record.scan_angle,
94 point_source_id: record.point_source_id,
95 gps_time: record.gps_time,
96 red: record.red,
97 green: record.green,
98 blue: record.blue,
99 })
100 }
101}
102
103#[derive(Clone, Debug)]
104struct OutputLasMetadata {
105 file_source_id: u16,
106 global_encoding: u16,
107 guid: [u8; 16],
108 system_identifier: String,
109 generating_software: String,
110 creation_day_of_year: u16,
111 creation_year: u16,
112 scale: (f64, f64, f64),
113 offset: Option<(f64, f64, f64)>,
114}
115
116impl Default for OutputLasMetadata {
117 fn default() -> Self {
118 Self {
119 file_source_id: 0,
120 global_encoding: 0,
121 guid: [0; 16],
122 system_identifier: "copc-rust".to_string(),
123 generating_software: "copc-writer".to_string(),
124 creation_day_of_year: 0,
125 creation_year: 2026,
126 scale: (0.001, 0.001, 0.001),
127 offset: None,
128 }
129 }
130}
131
132impl OutputLasMetadata {
133 fn from_las_header(header: &las::Header) -> Self {
134 let mut global_encoding = u16::from(header.gps_time_type());
135 if header.has_synthetic_return_numbers() {
136 global_encoding |= 8;
137 }
138 let transforms = header.transforms();
139 let (creation_day_of_year, creation_year) = header
140 .date()
141 .map(|date| {
142 let year = date.format("%Y").to_string().parse().unwrap_or(0);
143 let day = date.format("%j").to_string().parse().unwrap_or(0);
144 (day, year)
145 })
146 .unwrap_or((0, 0));
147
148 Self {
149 file_source_id: header.file_source_id(),
150 global_encoding,
151 guid: *header.guid().as_bytes(),
152 system_identifier: header.system_identifier().to_string(),
153 generating_software: header.generating_software().to_string(),
154 creation_day_of_year,
155 creation_year,
156 scale: (transforms.x.scale, transforms.y.scale, transforms.z.scale),
157 offset: Some((
158 transforms.x.offset,
159 transforms.y.offset,
160 transforms.z.offset,
161 )),
162 }
163 }
164}
165
166#[derive(Clone, Copy, Debug, PartialEq)]
167struct PointStats {
168 gpstime_min: f64,
169 gpstime_max: f64,
170 extended_return_counts: [u64; 15],
171}
172
173impl PointStats {
174 fn new() -> Self {
175 Self {
176 gpstime_min: f64::INFINITY,
177 gpstime_max: f64::NEG_INFINITY,
178 extended_return_counts: [0; 15],
179 }
180 }
181
182 fn record(&mut self, index: usize, fields: &CopcPointFields) -> Result<()> {
183 validate_finite_value(&format!("point {index} GPS time"), fields.gps_time)?;
184 self.gpstime_min = self.gpstime_min.min(fields.gps_time);
185 self.gpstime_max = self.gpstime_max.max(fields.gps_time);
186 if (1..=15).contains(&fields.return_number) {
187 self.extended_return_counts[usize::from(fields.return_number - 1)] += 1;
188 }
189 Ok(())
190 }
191}
192
193#[derive(Debug, Clone, Copy)]
194pub struct CopcWriterParams {
195 pub max_points_per_node: u32,
196 pub max_depth: u32,
197}
198
199impl Default for CopcWriterParams {
200 fn default() -> Self {
201 Self {
202 max_points_per_node: 100_000,
203 max_depth: 8,
204 }
205 }
206}
207
208pub fn write_source<S: CopcPointSource>(
209 path: &Path,
210 source: &S,
211 has_color: bool,
212 bounds: Bounds,
213 params: &CopcWriterParams,
214) -> Result<()> {
215 write_source_with_cancel(path, source, has_color, bounds, params, &NeverCancel)
216}
217
218pub fn write_source_with_cancel<S: CopcPointSource>(
219 path: &Path,
220 source: &S,
221 has_color: bool,
222 bounds: Bounds,
223 params: &CopcWriterParams,
224 cancel: &dyn CancelCheck,
225) -> Result<()> {
226 cancel.check()?;
227 if source.is_empty() {
228 return Err(Error::InvalidInput(
229 "cannot write empty cloud to COPC".into(),
230 ));
231 }
232 write_copc_inner(
233 path,
234 source,
235 has_color,
236 bounds,
237 params,
238 cancel,
239 &OutputLasMetadata::default(),
240 )
241}
242
243pub fn write_streaming_with_cancel<I>(
244 path: &Path,
245 layout: StreamingLayout,
246 points: I,
247 params: &CopcWriterParams,
248 spill_dir: &Path,
249 cancel: &dyn CancelCheck,
250) -> Result<()>
251where
252 I: IntoIterator<Item = Result<LasPointRecord>>,
253{
254 cancel.check()?;
255 validate_streaming_layout_supported(&layout)?;
256 let mut spill = SpillWriter::create(spill_dir, layout)?;
257 for (index, item) in points.into_iter().enumerate() {
258 if index % CANCEL_POLL_STRIDE == 0 {
259 cancel.check()?;
260 }
261 let record = item?;
262 validate_record_coordinates(&record, index)?;
263 spill.push(&record)?;
264 }
265 cancel.check()?;
266 let reader = spill.finalize()?;
267 write_copc_from_spill(path, reader, params, cancel, &OutputLasMetadata::default())
268}
269
270pub fn convert_las_to_copc_streaming(
271 las_path: &Path,
272 copc_path: &Path,
273 params: &CopcWriterParams,
274 spill_dir: &Path,
275 cancel: &dyn CancelCheck,
276) -> Result<()> {
277 cancel.check()?;
278 let mut reader = las::Reader::from_path(las_path).map_err(|e| Error::Las(e.to_string()))?;
279 validate_las_conversion_supported(reader.header())?;
280 let output_metadata = OutputLasMetadata::from_las_header(reader.header());
281 let layout = StreamingLayout::from_las_format(*reader.header().point_format());
282 let mut spill = SpillWriter::create(spill_dir, layout)?;
283 for (index, result) in reader.points().enumerate() {
284 if index % CANCEL_POLL_STRIDE == 0 {
285 cancel.check()?;
286 }
287 let point = result.map_err(|e| Error::Las(e.to_string()))?;
288 let record = LasPointRecord::from_las_point(&point);
289 validate_record_coordinates(&record, index)?;
290 spill.push(&record)?;
291 }
292 cancel.check()?;
293 let reader = spill.finalize()?;
294 write_copc_from_spill(copc_path, reader, params, cancel, &output_metadata)
295}
296
297fn validate_streaming_layout_supported(layout: &StreamingLayout) -> Result<()> {
298 let mut unsupported = Vec::new();
299 if layout.has_nir {
300 unsupported.push("NIR point data".to_string());
301 }
302 if layout.has_waveform {
303 unsupported.push("waveform point data".to_string());
304 }
305 if unsupported.is_empty() {
306 Ok(())
307 } else {
308 Err(Error::Unsupported(format!(
309 "COPC writer cannot preserve {}",
310 unsupported.join(", ")
311 )))
312 }
313}
314
315fn validate_las_conversion_supported(header: &las::Header) -> Result<()> {
316 let mut unsupported = Vec::new();
317 let format = header.point_format();
318 if format.has_nir {
319 unsupported.push("NIR point data".to_string());
320 }
321 if format.has_waveform {
322 unsupported.push("waveform point data".to_string());
323 }
324 if format.extra_bytes > 0 {
325 unsupported.push(format!("{} extra point byte(s)", format.extra_bytes));
326 }
327 let unsupported_vlr_count = header
328 .vlrs()
329 .iter()
330 .filter(|vlr| !is_laszip_vlr(vlr))
331 .count();
332 if unsupported_vlr_count > 0 {
333 unsupported.push(format!("{unsupported_vlr_count} VLR(s)"));
334 }
335 if !header.evlrs().is_empty() {
336 unsupported.push(format!("{} EVLR(s)", header.evlrs().len()));
337 }
338 if !header.padding().is_empty() {
339 unsupported.push(format!("{} header padding byte(s)", header.padding().len()));
340 }
341 if !header.vlr_padding().is_empty() {
342 unsupported.push(format!(
343 "{} VLR padding byte(s)",
344 header.vlr_padding().len()
345 ));
346 }
347 if !header.point_padding().is_empty() {
348 unsupported.push(format!(
349 "{} point padding byte(s)",
350 header.point_padding().len()
351 ));
352 }
353
354 if unsupported.is_empty() {
355 Ok(())
356 } else {
357 Err(Error::Unsupported(format!(
358 "LAS-to-COPC streaming conversion cannot preserve {}",
359 unsupported.join(", ")
360 )))
361 }
362}
363
364fn is_laszip_vlr(vlr: &las::Vlr) -> bool {
365 vlr.user_id == LASZIP_VLR_USER_ID && vlr.record_id == LASZIP_VLR_RECORD_ID
366}
367
368fn validate_record_coordinates(record: &LasPointRecord, index: usize) -> Result<()> {
369 validate_xyz_finite(index, record.x, record.y, record.z)
370}
371
372fn validate_coordinate_inputs<S: CopcPointSource>(
373 source: &S,
374 bounds: Bounds,
375 scale: (f64, f64, f64),
376 offset: (f64, f64, f64),
377 cancel: &dyn CancelCheck,
378) -> Result<PointStats> {
379 validate_bounds(bounds)?;
380 validate_transform(scale, offset)?;
381 let mut stats = PointStats::new();
382 for index in 0..source.len() {
383 if index % CANCEL_POLL_STRIDE == 0 {
384 cancel.check()?;
385 }
386 let (x, y, z) = source.xyz(index);
387 validate_xyz_finite(index, x, y, z)?;
388 quantize_xyz(index, x, y, z, scale, offset)?;
389
390 let fields = source.fields(index)?;
391 validate_xyz_finite(index, fields.x, fields.y, fields.z)?;
392 quantize_xyz(index, fields.x, fields.y, fields.z, scale, offset)?;
393 validate_scan_angle(index, fields.scan_angle)?;
394 validate_point_flags(index, &fields)?;
395 stats.record(index, &fields)?;
396 }
397 Ok(stats)
398}
399
400fn validate_point_flags(index: usize, fields: &CopcPointFields) -> Result<()> {
401 validate_point_field_range(index, "return_number", fields.return_number, 0, 15)?;
402 validate_point_field_range(index, "number_of_returns", fields.number_of_returns, 0, 15)?;
403 validate_point_field_range(index, "synthetic", fields.synthetic, 0, 1)?;
404 validate_point_field_range(index, "key_point", fields.key_point, 0, 1)?;
405 validate_point_field_range(index, "withheld", fields.withheld, 0, 1)?;
406 validate_point_field_range(index, "overlap", fields.overlap, 0, 1)?;
407 validate_point_field_range(index, "scan_channel", fields.scan_channel, 0, 3)?;
408 validate_point_field_range(
409 index,
410 "scan_direction_flag",
411 fields.scan_direction_flag,
412 0,
413 1,
414 )?;
415 validate_point_field_range(
416 index,
417 "edge_of_flight_line",
418 fields.edge_of_flight_line,
419 0,
420 1,
421 )
422}
423
424fn validate_scan_angle(index: usize, value: f32) -> Result<()> {
425 if !value.is_finite() {
426 return Err(Error::InvalidInput(format!(
427 "point {index} scan_angle must be finite, got {value}"
428 )));
429 }
430 let scaled = value / LAS_14_SCAN_ANGLE_SCALE;
431 if scaled < f32::from(i16::MIN) || scaled > f32::from(i16::MAX) {
432 return Err(Error::InvalidInput(format!(
433 "point {index} scan_angle {value} encodes to {scaled}, outside LAS i16 range"
434 )));
435 }
436 Ok(())
437}
438
439fn validate_point_field_range(index: usize, name: &str, value: u8, min: u8, max: u8) -> Result<()> {
440 if (min..=max).contains(&value) {
441 Ok(())
442 } else {
443 Err(Error::InvalidInput(format!(
444 "point {index} {name} must be in {min}..={max}, got {value}"
445 )))
446 }
447}
448
449fn validate_bounds(bounds: Bounds) -> Result<()> {
450 validate_finite_value("bounds min x", bounds.min.0)?;
451 validate_finite_value("bounds min y", bounds.min.1)?;
452 validate_finite_value("bounds min z", bounds.min.2)?;
453 validate_finite_value("bounds max x", bounds.max.0)?;
454 validate_finite_value("bounds max y", bounds.max.1)?;
455 validate_finite_value("bounds max z", bounds.max.2)?;
456 for (axis, min, max) in [
457 ("x", bounds.min.0, bounds.max.0),
458 ("y", bounds.min.1, bounds.max.1),
459 ("z", bounds.min.2, bounds.max.2),
460 ] {
461 if min > max {
462 return Err(Error::InvalidInput(format!(
463 "bounds {axis} min {min} exceeds max {max}"
464 )));
465 }
466 validate_finite_value(&format!("bounds {axis} span"), max - min)?;
467 }
468 Ok(())
469}
470
471fn validate_transform(scale: (f64, f64, f64), offset: (f64, f64, f64)) -> Result<()> {
472 for (axis, value) in [("x", scale.0), ("y", scale.1), ("z", scale.2)] {
473 if !value.is_finite() || value <= 0.0 {
474 return Err(Error::InvalidInput(format!(
475 "LAS {axis} scale must be finite and positive, got {value}"
476 )));
477 }
478 }
479 validate_finite_value("LAS x offset", offset.0)?;
480 validate_finite_value("LAS y offset", offset.1)?;
481 validate_finite_value("LAS z offset", offset.2)?;
482 Ok(())
483}
484
485fn validate_xyz_finite(index: usize, x: f64, y: f64, z: f64) -> Result<()> {
486 validate_point_axis_finite(index, "x", x)?;
487 validate_point_axis_finite(index, "y", y)?;
488 validate_point_axis_finite(index, "z", z)
489}
490
491fn validate_point_axis_finite(index: usize, axis: &str, value: f64) -> Result<()> {
492 if value.is_finite() {
493 Ok(())
494 } else {
495 Err(Error::InvalidInput(format!(
496 "point {index} {axis} coordinate must be finite, got {value}"
497 )))
498 }
499}
500
501fn validate_finite_value(name: &str, value: f64) -> Result<()> {
502 if value.is_finite() {
503 Ok(())
504 } else {
505 Err(Error::InvalidInput(format!(
506 "{name} must be finite, got {value}"
507 )))
508 }
509}
510
511fn quantize_xyz(
512 index: usize,
513 x: f64,
514 y: f64,
515 z: f64,
516 scale: (f64, f64, f64),
517 offset: (f64, f64, f64),
518) -> Result<(i32, i32, i32)> {
519 Ok((
520 quantize_axis(index, "x", x, scale.0, offset.0)?,
521 quantize_axis(index, "y", y, scale.1, offset.1)?,
522 quantize_axis(index, "z", z, scale.2, offset.2)?,
523 ))
524}
525
526fn quantize_axis(index: usize, axis: &str, value: f64, scale: f64, offset: f64) -> Result<i32> {
527 let scaled = ((value - offset) / scale).round();
528 if !scaled.is_finite() {
529 return Err(Error::InvalidInput(format!(
530 "point {index} {axis} coordinate cannot be encoded with scale {scale} and offset {offset}"
531 )));
532 }
533 if scaled < f64::from(i32::MIN) || scaled > f64::from(i32::MAX) {
534 return Err(Error::InvalidInput(format!(
535 "point {index} {axis} coordinate {value} encodes to {scaled}, outside LAS i32 range"
536 )));
537 }
538 Ok(scaled as i32)
539}
540
541fn write_copc_from_spill(
542 path: &Path,
543 reader: SpillReader,
544 params: &CopcWriterParams,
545 cancel: &dyn CancelCheck,
546 metadata: &OutputLasMetadata,
547) -> Result<()> {
548 cancel.check()?;
549 validate_streaming_layout_supported(&reader.layout())?;
550 if reader.is_empty() {
551 return Err(Error::InvalidInput(
552 "cannot write empty cloud to COPC".into(),
553 ));
554 }
555 let has_color = reader.layout().has_color;
556 let bounds = reader.bounds();
557 let source = SpillSource { reader: &reader };
558 write_copc_inner(path, &source, has_color, bounds, params, cancel, metadata)
559}
560
561fn write_copc_inner<S: CopcPointSource>(
562 path: &Path,
563 source: &S,
564 has_color: bool,
565 bounds: Bounds,
566 params: &CopcWriterParams,
567 cancel: &dyn CancelCheck,
568 metadata: &OutputLasMetadata,
569) -> Result<()> {
570 cancel.check()?;
571 let point_format_id = if has_color { 7u8 } else { 6u8 };
572 let point_format =
573 LasFormat::new(point_format_id).map_err(|e| Error::Las(format!("point format: {e}")))?;
574 let point_record_length = point_format.len();
575
576 let (scale_x, scale_y, scale_z) = metadata.scale;
577 let (offset_x, offset_y, offset_z) =
578 metadata
579 .offset
580 .unwrap_or((bounds.min.0, bounds.min.1, bounds.min.2));
581 let point_stats = validate_coordinate_inputs(
582 source,
583 bounds,
584 (scale_x, scale_y, scale_z),
585 (offset_x, offset_y, offset_z),
586 cancel,
587 )?;
588 let (center, halfsize) = cube_from_bounds(&bounds);
589
590 let lod_index = build_lod_index(source, center, halfsize, params, cancel)?;
591 cancel.check()?;
592
593 let var_vlr = LazVlrBuilder::default()
594 .with_point_format(point_format_id, 0)
595 .map_err(|e| Error::Las(format!("laz items: {e}")))?
596 .with_variable_chunk_size()
597 .build();
598 let mut var_vlr_bytes = Vec::new();
599 var_vlr
600 .write_to(&mut var_vlr_bytes)
601 .map_err(|e| Error::Las(format!("variable chunk LAZ VLR: {e}")))?;
602
603 let copc_info_vlr_size = 160u16;
604 let las_header_size = 375u32;
605 let total_vlr_bytes =
606 (54u32 + u32::from(copc_info_vlr_size)) + (54u32 + var_vlr_bytes.len() as u32);
607 let offset_to_point_data = las_header_size + total_vlr_bytes;
608
609 let file = File::create(path).map_err(|e| Error::io("create COPC file", e))?;
610 let mut writer = BufWriter::new(file);
611
612 let header = LasHeader {
613 point_data_format: point_format_id | 0x80,
614 point_record_length,
615 offset_to_point_data,
616 number_of_vlrs: 2,
617 file_source_id: metadata.file_source_id,
618 global_encoding: metadata.global_encoding,
619 guid: metadata.guid,
620 system_identifier: metadata.system_identifier.clone(),
621 generating_software: metadata.generating_software.clone(),
622 creation_day_of_year: metadata.creation_day_of_year,
623 creation_year: metadata.creation_year,
624 scale: (scale_x, scale_y, scale_z),
625 offset: (offset_x, offset_y, offset_z),
626 bounds,
627 legacy_point_count: 0,
628 total_point_count: source.len() as u64,
629 offset_to_first_evlr: 0,
630 number_of_evlrs: 1,
631 extended_return_counts: point_stats.extended_return_counts,
632 };
633 header.write(&mut writer)?;
634
635 write_vlr_header(&mut writer, "copc", 1, copc_info_vlr_size, "COPC info")?;
636 let copc_info_payload_start = writer
637 .stream_position()
638 .map_err(|e| Error::io("record COPC info payload offset", e))?;
639 writer
640 .write_all(&[0u8; 160])
641 .map_err(|e| Error::io("write COPC info placeholder", e))?;
642
643 write_vlr_header(
644 &mut writer,
645 LASZIP_VLR_USER_ID,
646 LASZIP_VLR_RECORD_ID,
647 var_vlr_bytes.len() as u16,
648 "http://laszip.org",
649 )?;
650 writer
651 .write_all(&var_vlr_bytes)
652 .map_err(|e| Error::io("write LAZ VLR", e))?;
653
654 let point_data_actual_start = writer
655 .stream_position()
656 .map_err(|e| Error::io("record point data offset", e))?;
657 if point_data_actual_start as u32 != offset_to_point_data {
658 return Err(Error::InvalidInput(format!(
659 "VLR size accounting mismatch: at {point_data_actual_start}, expected {offset_to_point_data}"
660 )));
661 }
662
663 let mut compressor = LasZipCompressor::new(&mut writer, var_vlr.clone())
664 .map_err(|e| Error::Las(format!("compressor: {e}")))?;
665 let mut hierarchy: Vec<Entry> = Vec::with_capacity(lod_index.nodes.len());
666 let order_path: &Path = lod_index.order_path.as_ref();
667 let mut index_reader =
668 BufReader::new(File::open(order_path).map_err(|e| Error::io("open LOD order", e))?);
669 let mut point_buf = vec![0u8; point_record_length as usize];
670 let mut chunk_start_file_offset = compressor
671 .get_mut()
672 .stream_position()
673 .map_err(|e| Error::io("record chunk start", e))?;
674 chunk_start_file_offset += 8;
675
676 for node in &lod_index.nodes {
677 cancel.check()?;
678 index_reader
679 .seek(SeekFrom::Start(node.start))
680 .map_err(|e| Error::io("seek LOD order", e))?;
681 for point_index in 0..node.count {
682 if point_index % CANCEL_POLL_STRIDE == 0 {
683 cancel.check()?;
684 }
685 let source_index = index_reader
686 .read_u32::<LittleEndian>()
687 .map_err(|e| Error::io("read LOD order", e))?
688 as usize;
689 let fields = source.fields(source_index as usize)?;
690 encode_point_record(
691 &mut point_buf,
692 &fields,
693 (scale_x, scale_y, scale_z),
694 (offset_x, offset_y, offset_z),
695 source_index as usize,
696 &point_format,
697 )?;
698 compressor
699 .compress_one(&point_buf)
700 .map_err(|e| Error::Las(format!("compress point: {e}")))?;
701 }
702 compressor
703 .finish_current_chunk()
704 .map_err(|e| Error::Las(format!("finish chunk: {e}")))?;
705 let after = compressor
706 .get_mut()
707 .stream_position()
708 .map_err(|e| Error::io("record chunk end", e))?;
709 let byte_size = i32::try_from(after - chunk_start_file_offset)
710 .map_err(|_| Error::InvalidInput("LAZ chunk exceeds COPC i32 byte size".into()))?;
711 let point_count = i32::try_from(node.count)
712 .map_err(|_| Error::InvalidInput("node point count exceeds COPC i32 range".into()))?;
713 hierarchy.push(Entry {
714 key: node.key,
715 offset: chunk_start_file_offset,
716 byte_size,
717 point_count,
718 });
719 chunk_start_file_offset = after;
720 }
721
722 cancel.check()?;
723 compressor
724 .done()
725 .map_err(|e| Error::Las(format!("finish compressor: {e}")))?;
726 drop(compressor);
727
728 let evlr_start = writer
729 .stream_position()
730 .map_err(|e| Error::io("record EVLR start", e))?;
731 let root_hier_offset = evlr_start
732 .checked_add(60)
733 .ok_or_else(|| Error::InvalidInput("hierarchy EVLR offset overflow".into()))?;
734 let mut hierarchy_pages = plan_hierarchy_pages(&hierarchy, VoxelKey::root())?;
735 let hierarchy_end = assign_hierarchy_page_offsets(&mut hierarchy_pages, root_hier_offset)?;
736 let hierarchy_body_size = hierarchy_end
737 .checked_sub(root_hier_offset)
738 .ok_or_else(|| Error::InvalidInput("hierarchy size overflow".into()))?;
739 write_evlr_header(
740 &mut writer,
741 "copc",
742 1000,
743 hierarchy_body_size,
744 "COPC hierarchy",
745 )?;
746 let actual_root_hier_offset = writer
747 .stream_position()
748 .map_err(|e| Error::io("record root hierarchy offset", e))?;
749 if actual_root_hier_offset != root_hier_offset {
750 return Err(Error::InvalidInput(format!(
751 "hierarchy offset accounting mismatch: at {actual_root_hier_offset}, expected {root_hier_offset}"
752 )));
753 }
754 write_hierarchy_page_tree(&mut writer, &hierarchy_pages)?;
755
756 writer
757 .seek(SeekFrom::Start(copc_info_payload_start))
758 .map_err(|e| Error::io("seek COPC info payload", e))?;
759 let info = CopcInfo {
760 center,
761 halfsize,
762 spacing: halfsize / 128.0,
763 root_hier_offset,
764 root_hier_size: hierarchy_pages.byte_size,
765 gpstime_min: point_stats.gpstime_min,
766 gpstime_max: point_stats.gpstime_max,
767 };
768 writer
769 .write_all(&info.write_le_bytes())
770 .map_err(|e| Error::io("patch COPC info", e))?;
771
772 writer
773 .seek(SeekFrom::Start(235))
774 .map_err(|e| Error::io("seek first EVLR offset", e))?;
775 writer
776 .write_u64::<LittleEndian>(evlr_start)
777 .map_err(|e| Error::io("patch first EVLR offset", e))?;
778
779 writer
780 .flush()
781 .map_err(|e| Error::io("flush COPC file", e))?;
782 Ok(())
783}
784
785#[derive(Debug)]
786struct HierarchyPagePlan {
787 key: VoxelKey,
788 items: Vec<HierarchyPageItem>,
789 offset: u64,
790 byte_size: u64,
791}
792
793#[derive(Debug)]
794enum HierarchyPageItem {
795 Point(Entry),
796 Child(Box<HierarchyPagePlan>),
797}
798
799fn plan_hierarchy_pages(entries: &[Entry], key: VoxelKey) -> Result<HierarchyPagePlan> {
800 if entries.is_empty() {
801 return Err(Error::InvalidInput(
802 "cannot write empty hierarchy page".into(),
803 ));
804 }
805 if entries.len() <= HIERARCHY_PAGE_MAX_ENTRIES {
806 return Ok(HierarchyPagePlan {
807 key,
808 items: entries
809 .iter()
810 .copied()
811 .map(HierarchyPageItem::Point)
812 .collect(),
813 offset: 0,
814 byte_size: 0,
815 });
816 }
817
818 let mut point_entry = None;
819 let mut child_entries: [Vec<Entry>; 8] = std::array::from_fn(|_| Vec::new());
820 for entry in entries.iter().copied() {
821 if entry.key == key {
822 point_entry = Some(entry);
823 continue;
824 }
825 let mut matched = false;
826 for (octant, child_entries) in child_entries.iter_mut().enumerate() {
827 let child_key = key.child(octant as u8);
828 if key_contains(child_key, entry.key) {
829 child_entries.push(entry);
830 matched = true;
831 break;
832 }
833 }
834 if !matched {
835 return Err(Error::InvalidInput(format!(
836 "hierarchy entry {:?} is not under page key {:?}",
837 entry.key, key
838 )));
839 }
840 }
841
842 let mut items = Vec::new();
843 if let Some(entry) = point_entry {
844 items.push(HierarchyPageItem::Point(entry));
845 }
846 for (octant, child_entries) in child_entries.into_iter().enumerate() {
847 if child_entries.is_empty() {
848 continue;
849 }
850 items.push(HierarchyPageItem::Child(Box::new(plan_hierarchy_pages(
851 &child_entries,
852 key.child(octant as u8),
853 )?)));
854 }
855 if items.len() > HIERARCHY_PAGE_MAX_ENTRIES {
856 return Err(Error::InvalidInput(format!(
857 "hierarchy page for {:?} has {} entries, max is {}",
858 key,
859 items.len(),
860 HIERARCHY_PAGE_MAX_ENTRIES
861 )));
862 }
863 Ok(HierarchyPagePlan {
864 key,
865 items,
866 offset: 0,
867 byte_size: 0,
868 })
869}
870
871fn assign_hierarchy_page_offsets(page: &mut HierarchyPagePlan, offset: u64) -> Result<u64> {
872 page.offset = offset;
873 page.byte_size = hierarchy_page_byte_size(page.items.len())?;
874 let mut next = offset
875 .checked_add(page.byte_size)
876 .ok_or_else(|| Error::InvalidInput("hierarchy page offset overflow".into()))?;
877 for item in &mut page.items {
878 if let HierarchyPageItem::Child(child) = item {
879 next = assign_hierarchy_page_offsets(child, next)?;
880 }
881 }
882 Ok(next)
883}
884
885fn hierarchy_page_byte_size(entry_count: usize) -> Result<u64> {
886 let bytes = entry_count
887 .checked_mul(HIERARCHY_ENTRY_BYTES)
888 .ok_or_else(|| Error::InvalidInput("hierarchy page size overflow".into()))?;
889 u64::try_from(bytes).map_err(|_| Error::InvalidInput("hierarchy page is too large".into()))
890}
891
892fn write_hierarchy_page_tree<W: Write + Seek>(
893 writer: &mut W,
894 page: &HierarchyPagePlan,
895) -> Result<()> {
896 let position = writer
897 .stream_position()
898 .map_err(|e| Error::io("record hierarchy page offset", e))?;
899 if position != page.offset {
900 return Err(Error::InvalidInput(format!(
901 "hierarchy page offset mismatch: at {position}, expected {}",
902 page.offset
903 )));
904 }
905 let mut entry_buf = [0u8; HIERARCHY_ENTRY_BYTES];
906 for item in &page.items {
907 hierarchy_page_item_entry(item)?.write_le(&mut entry_buf)?;
908 writer
909 .write_all(&entry_buf)
910 .map_err(|e| Error::io("write hierarchy entry", e))?;
911 }
912 for item in &page.items {
913 if let HierarchyPageItem::Child(child) = item {
914 write_hierarchy_page_tree(writer, child)?;
915 }
916 }
917 Ok(())
918}
919
920fn hierarchy_page_item_entry(item: &HierarchyPageItem) -> Result<Entry> {
921 match item {
922 HierarchyPageItem::Point(entry) => Ok(*entry),
923 HierarchyPageItem::Child(child) => Ok(Entry {
924 key: child.key,
925 offset: child.offset,
926 byte_size: i32::try_from(child.byte_size).map_err(|_| {
927 Error::InvalidInput("child hierarchy page exceeds COPC i32 byte size".into())
928 })?,
929 point_count: -1,
930 }),
931 }
932}
933
934fn key_contains(ancestor: VoxelKey, key: VoxelKey) -> bool {
935 if key.level < ancestor.level {
936 return false;
937 }
938 let shift = (key.level - ancestor.level) as u32;
939 (key.x >> shift) == ancestor.x
940 && (key.y >> shift) == ancestor.y
941 && (key.z >> shift) == ancestor.z
942}
943
944struct LodIndex {
945 nodes: Vec<LodNodeRange>,
946 order_path: TempPath,
947}
948
949#[derive(Clone, Copy, Debug, PartialEq, Eq)]
950struct LodNodeRange {
951 key: VoxelKey,
952 start: u64,
953 count: usize,
954}
955
956struct IndexRun {
957 path: TempPath,
958 start: u64,
959 count: usize,
960}
961
962fn build_lod_index<S: CopcPointSource>(
963 source: &S,
964 center: (f64, f64, f64),
965 halfsize: f64,
966 params: &CopcWriterParams,
967 cancel: &dyn CancelCheck,
968) -> Result<LodIndex> {
969 cancel.check()?;
970 let total_points = u32::try_from(source.len()).map_err(|_| {
971 Error::InvalidInput("COPC writer supports at most u32::MAX points per file".into())
972 })?;
973 let max_points_per_node = params.max_points_per_node.max(1) as usize;
974 let max_depth = params.max_depth.min(30);
975 let root_run = write_root_index_run(total_points, cancel)?;
976 let mut order_file = new_index_tempfile("order")?;
977 let mut order_offset = 0;
978 let mut nodes = Vec::new();
979 {
980 let mut order_writer = BufWriter::new(order_file.as_file_mut());
981 let mut builder = LodIndexBuilder {
982 source,
983 max_points_per_node,
984 max_depth,
985 cancel,
986 order_writer: &mut order_writer,
987 order_offset: &mut order_offset,
988 nodes: &mut nodes,
989 };
990 builder.assign(VoxelKey::root(), root_run, Bounds::cube(center, halfsize))?;
991 order_writer
992 .flush()
993 .map_err(|e| Error::io("flush LOD index order", e))?;
994 }
995 nodes.sort_by_key(|node| node.key);
996 Ok(LodIndex {
997 nodes,
998 order_path: order_file.into_temp_path(),
999 })
1000}
1001
1002struct LodIndexBuilder<'a, S: CopcPointSource, W: Write> {
1003 source: &'a S,
1004 max_points_per_node: usize,
1005 max_depth: u32,
1006 cancel: &'a dyn CancelCheck,
1007 order_writer: &'a mut W,
1008 order_offset: &'a mut u64,
1009 nodes: &'a mut Vec<LodNodeRange>,
1010}
1011
1012impl<S: CopcPointSource, W: Write> LodIndexBuilder<'_, S, W> {
1013 fn assign(&mut self, key: VoxelKey, run: IndexRun, bounds: Bounds) -> Result<()> {
1014 self.cancel.check()?;
1015 if run.count == 0 {
1016 return Ok(());
1017 }
1018 if run.count <= self.max_points_per_node || key.level as u32 >= self.max_depth {
1019 let start = *self.order_offset;
1020 append_index_run_to_order(&run, self.order_writer, self.order_offset, self.cancel)?;
1021 self.nodes.push(LodNodeRange {
1022 key,
1023 start,
1024 count: run.count,
1025 });
1026 return Ok(());
1027 }
1028
1029 let mut children = partition_index_run(self.source, &run, bounds, self.cancel)?;
1030 let start = *self.order_offset;
1031 let selected_counts = append_lod_selection_to_order(
1032 &children,
1033 self.max_points_per_node,
1034 self.order_writer,
1035 self.order_offset,
1036 self.cancel,
1037 )?;
1038 let selected_total = selected_counts.iter().sum();
1039 self.nodes.push(LodNodeRange {
1040 key,
1041 start,
1042 count: selected_total,
1043 });
1044
1045 for (octant, child) in children.iter_mut().enumerate() {
1046 let Some(mut child_run) = child.take() else {
1047 continue;
1048 };
1049 let selected = selected_counts[octant];
1050 if selected >= child_run.count {
1051 continue;
1052 }
1053 child_run.start += selected as u64 * INDEX_RECORD_BYTES;
1054 child_run.count -= selected;
1055 self.assign(
1056 key.child(octant as u8),
1057 child_run,
1058 bounds.octant(octant as u8),
1059 )?;
1060 }
1061 Ok(())
1062 }
1063}
1064
1065fn write_root_index_run(total_points: u32, cancel: &dyn CancelCheck) -> Result<IndexRun> {
1066 let mut writer = BufWriter::new(new_index_tempfile("root")?);
1067 for index in 0..total_points {
1068 if index as usize % CANCEL_POLL_STRIDE == 0 {
1069 cancel.check()?;
1070 }
1071 writer
1072 .write_u32::<LittleEndian>(index)
1073 .map_err(|e| Error::io("write root LOD index", e))?;
1074 }
1075 let file = writer
1076 .into_inner()
1077 .map_err(|e| Error::io("flush root LOD index", e.into_error()))?;
1078 Ok(IndexRun {
1079 path: file.into_temp_path(),
1080 start: 0,
1081 count: total_points as usize,
1082 })
1083}
1084
1085fn partition_index_run<S: CopcPointSource>(
1086 source: &S,
1087 run: &IndexRun,
1088 bounds: Bounds,
1089 cancel: &dyn CancelCheck,
1090) -> Result<[Option<IndexRun>; 8]> {
1091 let mut reader = open_index_run(run)?;
1092 let mut writers: [Option<BufWriter<NamedTempFile>>; 8] = std::array::from_fn(|_| None);
1093 let mut counts = [0usize; 8];
1094 for read_index in 0..run.count {
1095 if read_index % CANCEL_POLL_STRIDE == 0 {
1096 cancel.check()?;
1097 }
1098 let index = reader
1099 .read_u32::<LittleEndian>()
1100 .map_err(|e| Error::io("read LOD partition index", e))?;
1101 let (x, y, z) = source.xyz(index as usize);
1102 let octant = child_octant(bounds, x, y, z);
1103 if writers[octant].is_none() {
1104 writers[octant] = Some(BufWriter::new(new_index_tempfile("partition")?));
1105 }
1106 writers[octant]
1107 .as_mut()
1108 .expect("partition writer exists")
1109 .write_u32::<LittleEndian>(index)
1110 .map_err(|e| Error::io("write LOD partition index", e))?;
1111 counts[octant] += 1;
1112 }
1113
1114 let mut children: [Option<IndexRun>; 8] = std::array::from_fn(|_| None);
1115 for octant in 0..8 {
1116 let Some(writer) = writers[octant].take() else {
1117 continue;
1118 };
1119 let file = writer
1120 .into_inner()
1121 .map_err(|e| Error::io("flush LOD partition index", e.into_error()))?;
1122 children[octant] = Some(IndexRun {
1123 path: file.into_temp_path(),
1124 start: 0,
1125 count: counts[octant],
1126 });
1127 }
1128 Ok(children)
1129}
1130
1131fn append_lod_selection_to_order<W: Write>(
1132 children: &[Option<IndexRun>; 8],
1133 max_points_per_node: usize,
1134 order_writer: &mut W,
1135 order_offset: &mut u64,
1136 cancel: &dyn CancelCheck,
1137) -> Result<[usize; 8]> {
1138 let mut readers: [Option<BufReader<File>>; 8] = std::array::from_fn(|_| None);
1139 for octant in 0..8 {
1140 if let Some(child) = &children[octant] {
1141 readers[octant] = Some(open_index_run(child)?);
1142 }
1143 }
1144
1145 let mut selected_counts = [0usize; 8];
1146 let mut selected_total = 0usize;
1147 while selected_total < max_points_per_node {
1148 cancel.check()?;
1149 let mut progressed = false;
1150 for octant in 0..8 {
1151 let Some(child) = &children[octant] else {
1152 continue;
1153 };
1154 if selected_counts[octant] >= child.count {
1155 continue;
1156 }
1157 let index = readers[octant]
1158 .as_mut()
1159 .expect("partition reader exists")
1160 .read_u32::<LittleEndian>()
1161 .map_err(|e| Error::io("read selected LOD index", e))?;
1162 append_index_to_order(order_writer, order_offset, index)?;
1163 selected_counts[octant] += 1;
1164 selected_total += 1;
1165 progressed = true;
1166 if selected_total == max_points_per_node {
1167 break;
1168 }
1169 }
1170 if !progressed {
1171 break;
1172 }
1173 }
1174 Ok(selected_counts)
1175}
1176
1177fn append_index_run_to_order<W: Write>(
1178 run: &IndexRun,
1179 order_writer: &mut W,
1180 order_offset: &mut u64,
1181 cancel: &dyn CancelCheck,
1182) -> Result<()> {
1183 let mut reader = open_index_run(run)?;
1184 for read_index in 0..run.count {
1185 if read_index % CANCEL_POLL_STRIDE == 0 {
1186 cancel.check()?;
1187 }
1188 let index = reader
1189 .read_u32::<LittleEndian>()
1190 .map_err(|e| Error::io("read LOD index", e))?;
1191 append_index_to_order(order_writer, order_offset, index)?;
1192 }
1193 Ok(())
1194}
1195
1196fn append_index_to_order<W: Write>(
1197 order_writer: &mut W,
1198 order_offset: &mut u64,
1199 index: u32,
1200) -> Result<()> {
1201 order_writer
1202 .write_u32::<LittleEndian>(index)
1203 .map_err(|e| Error::io("write LOD index order", e))?;
1204 *order_offset = order_offset
1205 .checked_add(INDEX_RECORD_BYTES)
1206 .ok_or_else(|| Error::InvalidInput("LOD index order exceeds u64 range".into()))?;
1207 Ok(())
1208}
1209
1210fn open_index_run(run: &IndexRun) -> Result<BufReader<File>> {
1211 let path: &Path = run.path.as_ref();
1212 let mut file = File::open(path).map_err(|e| Error::io("open LOD index", e))?;
1213 file.seek(SeekFrom::Start(run.start))
1214 .map_err(|e| Error::io("seek LOD index", e))?;
1215 Ok(BufReader::new(file))
1216}
1217
1218fn new_index_tempfile(label: &str) -> Result<NamedTempFile> {
1219 let prefix = format!(".copc-writer-{label}.");
1220 tempfile::Builder::new()
1221 .prefix(&prefix)
1222 .suffix(".idx")
1223 .tempfile()
1224 .map_err(|e| Error::io("create LOD index file", e))
1225}
1226
1227fn child_octant(bounds: Bounds, x: f64, y: f64, z: f64) -> usize {
1228 let center = bounds.center();
1229 usize::from(x >= center.0)
1230 | (usize::from(y >= center.1) << 1)
1231 | (usize::from(z >= center.2) << 2)
1232}
1233
1234fn cube_from_bounds(bounds: &Bounds) -> ((f64, f64, f64), f64) {
1235 let dx = bounds.max.0 - bounds.min.0;
1236 let dy = bounds.max.1 - bounds.min.1;
1237 let dz = bounds.max.2 - bounds.min.2;
1238 let center = (
1239 bounds.min.0 + dx * 0.5,
1240 bounds.min.1 + dy * 0.5,
1241 bounds.min.2 + dz * 0.5,
1242 );
1243 let halfsize = (dx.max(dy).max(dz) * 0.5).max(1e-6);
1244 (center, halfsize)
1245}
1246
1247struct LasHeader {
1248 point_data_format: u8,
1249 point_record_length: u16,
1250 offset_to_point_data: u32,
1251 number_of_vlrs: u32,
1252 file_source_id: u16,
1253 global_encoding: u16,
1254 guid: [u8; 16],
1255 system_identifier: String,
1256 generating_software: String,
1257 creation_day_of_year: u16,
1258 creation_year: u16,
1259 scale: (f64, f64, f64),
1260 offset: (f64, f64, f64),
1261 bounds: Bounds,
1262 legacy_point_count: u32,
1263 total_point_count: u64,
1264 offset_to_first_evlr: u64,
1265 number_of_evlrs: u32,
1266 extended_return_counts: [u64; 15],
1267}
1268
1269impl LasHeader {
1270 fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
1271 writer
1272 .write_all(b"LASF")
1273 .map_err(|e| Error::io("write LAS signature", e))?;
1274 writer
1275 .write_u16::<LittleEndian>(self.file_source_id)
1276 .map_err(|e| Error::io("write file source id", e))?;
1277 writer
1278 .write_u16::<LittleEndian>(self.global_encoding)
1279 .map_err(|e| Error::io("write global encoding", e))?;
1280 writer
1281 .write_all(&self.guid)
1282 .map_err(|e| Error::io("write GUID", e))?;
1283 writer
1284 .write_u8(1)
1285 .map_err(|e| Error::io("write version major", e))?;
1286 writer
1287 .write_u8(4)
1288 .map_err(|e| Error::io("write version minor", e))?;
1289 writer
1290 .write_all(&pad(self.system_identifier.as_bytes(), 32))
1291 .map_err(|e| Error::io("write system id", e))?;
1292 writer
1293 .write_all(&pad(self.generating_software.as_bytes(), 32))
1294 .map_err(|e| Error::io("write generating software", e))?;
1295 writer
1296 .write_u16::<LittleEndian>(self.creation_day_of_year)
1297 .map_err(|e| Error::io("write creation day", e))?;
1298 writer
1299 .write_u16::<LittleEndian>(self.creation_year)
1300 .map_err(|e| Error::io("write creation year", e))?;
1301 writer
1302 .write_u16::<LittleEndian>(375)
1303 .map_err(|e| Error::io("write header size", e))?;
1304 writer
1305 .write_u32::<LittleEndian>(self.offset_to_point_data)
1306 .map_err(|e| Error::io("write point data offset", e))?;
1307 writer
1308 .write_u32::<LittleEndian>(self.number_of_vlrs)
1309 .map_err(|e| Error::io("write VLR count", e))?;
1310 writer
1311 .write_u8(self.point_data_format)
1312 .map_err(|e| Error::io("write point format", e))?;
1313 writer
1314 .write_u16::<LittleEndian>(self.point_record_length)
1315 .map_err(|e| Error::io("write point record length", e))?;
1316 writer
1317 .write_u32::<LittleEndian>(self.legacy_point_count)
1318 .map_err(|e| Error::io("write legacy point count", e))?;
1319 for _ in 0..5 {
1320 writer
1321 .write_u32::<LittleEndian>(0)
1322 .map_err(|e| Error::io("write legacy returns", e))?;
1323 }
1324 writer
1325 .write_f64::<LittleEndian>(self.scale.0)
1326 .map_err(|e| Error::io("write x scale", e))?;
1327 writer
1328 .write_f64::<LittleEndian>(self.scale.1)
1329 .map_err(|e| Error::io("write y scale", e))?;
1330 writer
1331 .write_f64::<LittleEndian>(self.scale.2)
1332 .map_err(|e| Error::io("write z scale", e))?;
1333 writer
1334 .write_f64::<LittleEndian>(self.offset.0)
1335 .map_err(|e| Error::io("write x offset", e))?;
1336 writer
1337 .write_f64::<LittleEndian>(self.offset.1)
1338 .map_err(|e| Error::io("write y offset", e))?;
1339 writer
1340 .write_f64::<LittleEndian>(self.offset.2)
1341 .map_err(|e| Error::io("write z offset", e))?;
1342 writer
1343 .write_f64::<LittleEndian>(self.bounds.max.0)
1344 .map_err(|e| Error::io("write max x", e))?;
1345 writer
1346 .write_f64::<LittleEndian>(self.bounds.min.0)
1347 .map_err(|e| Error::io("write min x", e))?;
1348 writer
1349 .write_f64::<LittleEndian>(self.bounds.max.1)
1350 .map_err(|e| Error::io("write max y", e))?;
1351 writer
1352 .write_f64::<LittleEndian>(self.bounds.min.1)
1353 .map_err(|e| Error::io("write min y", e))?;
1354 writer
1355 .write_f64::<LittleEndian>(self.bounds.max.2)
1356 .map_err(|e| Error::io("write max z", e))?;
1357 writer
1358 .write_f64::<LittleEndian>(self.bounds.min.2)
1359 .map_err(|e| Error::io("write min z", e))?;
1360 writer
1361 .write_u64::<LittleEndian>(0)
1362 .map_err(|e| Error::io("write waveform packet start", e))?;
1363 writer
1364 .write_u64::<LittleEndian>(self.offset_to_first_evlr)
1365 .map_err(|e| Error::io("write first EVLR offset", e))?;
1366 writer
1367 .write_u32::<LittleEndian>(self.number_of_evlrs)
1368 .map_err(|e| Error::io("write EVLR count", e))?;
1369 writer
1370 .write_u64::<LittleEndian>(self.total_point_count)
1371 .map_err(|e| Error::io("write total point count", e))?;
1372 for count in self.extended_return_counts {
1373 writer
1374 .write_u64::<LittleEndian>(count)
1375 .map_err(|e| Error::io("write extended returns", e))?;
1376 }
1377 Ok(())
1378 }
1379}
1380
1381fn pad(value: &[u8], len: usize) -> Vec<u8> {
1382 let mut out = Vec::with_capacity(len);
1383 let take = value.len().min(len);
1384 out.extend_from_slice(&value[..take]);
1385 out.resize(len, 0);
1386 out
1387}
1388
1389fn write_vlr_header<W: Write>(
1390 writer: &mut W,
1391 user_id: &str,
1392 record_id: u16,
1393 body_size: u16,
1394 description: &str,
1395) -> Result<()> {
1396 writer
1397 .write_u16::<LittleEndian>(0)
1398 .map_err(|e| Error::io("write VLR reserved", e))?;
1399 writer
1400 .write_all(&pad(user_id.as_bytes(), 16))
1401 .map_err(|e| Error::io("write VLR user id", e))?;
1402 writer
1403 .write_u16::<LittleEndian>(record_id)
1404 .map_err(|e| Error::io("write VLR record id", e))?;
1405 writer
1406 .write_u16::<LittleEndian>(body_size)
1407 .map_err(|e| Error::io("write VLR body size", e))?;
1408 writer
1409 .write_all(&pad(description.as_bytes(), 32))
1410 .map_err(|e| Error::io("write VLR description", e))?;
1411 Ok(())
1412}
1413
1414fn write_evlr_header<W: Write>(
1415 writer: &mut W,
1416 user_id: &str,
1417 record_id: u16,
1418 body_size: u64,
1419 description: &str,
1420) -> Result<()> {
1421 writer
1422 .write_u16::<LittleEndian>(0)
1423 .map_err(|e| Error::io("write EVLR reserved", e))?;
1424 writer
1425 .write_all(&pad(user_id.as_bytes(), 16))
1426 .map_err(|e| Error::io("write EVLR user id", e))?;
1427 writer
1428 .write_u16::<LittleEndian>(record_id)
1429 .map_err(|e| Error::io("write EVLR record id", e))?;
1430 writer
1431 .write_u64::<LittleEndian>(body_size)
1432 .map_err(|e| Error::io("write EVLR body size", e))?;
1433 writer
1434 .write_all(&pad(description.as_bytes(), 32))
1435 .map_err(|e| Error::io("write EVLR description", e))?;
1436 Ok(())
1437}
1438
1439fn encode_point_record(
1440 buf: &mut [u8],
1441 fields: &CopcPointFields,
1442 scale: (f64, f64, f64),
1443 offset: (f64, f64, f64),
1444 point_index: usize,
1445 format: &LasFormat,
1446) -> Result<()> {
1447 let mut cursor = Cursor::new(buf);
1448 let (ix, iy, iz) = quantize_xyz(point_index, fields.x, fields.y, fields.z, scale, offset)?;
1449 let flags =
1450 fields.synthetic | (fields.key_point << 1) | (fields.withheld << 2) | (fields.overlap << 3);
1451 let point = raw::Point {
1452 x: ix,
1453 y: iy,
1454 z: iz,
1455 intensity: fields.intensity,
1456 flags: raw::point::Flags::ThreeByte(
1457 fields.return_number | (fields.number_of_returns << 4),
1458 flags
1459 | (fields.scan_channel << 4)
1460 | (fields.scan_direction_flag << 6)
1461 | (fields.edge_of_flight_line << 7),
1462 fields.classification,
1463 ),
1464 scan_angle: raw::point::ScanAngle::from(fields.scan_angle),
1465 user_data: fields.user_data,
1466 point_source_id: fields.point_source_id,
1467 gps_time: Some(fields.gps_time),
1468 color: format
1469 .has_color
1470 .then_some(Color::new(fields.red, fields.green, fields.blue)),
1471 waveform: None,
1472 nir: None,
1473 extra_bytes: Vec::new(),
1474 };
1475 point
1476 .write_to(&mut cursor, format)
1477 .map_err(|e| Error::Las(format!("write point record: {e}")))?;
1478 Ok(())
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483 use super::*;
1484
1485 struct VecSource {
1486 points: Vec<CopcPointFields>,
1487 }
1488
1489 impl CopcPointSource for VecSource {
1490 fn len(&self) -> usize {
1491 self.points.len()
1492 }
1493
1494 fn xyz(&self, index: usize) -> (f64, f64, f64) {
1495 let point = self.points[index];
1496 (point.x, point.y, point.z)
1497 }
1498
1499 fn fields(&self, index: usize) -> Result<CopcPointFields> {
1500 Ok(self.points[index])
1501 }
1502 }
1503
1504 #[test]
1505 fn spooled_lod_index_covers_each_point_once() {
1506 let points = (0..257)
1507 .map(|i| CopcPointFields {
1508 x: f64::from((i * 37) % 101),
1509 y: f64::from((i * 53) % 103),
1510 z: f64::from((i * 71) % 107),
1511 intensity: 0,
1512 return_number: 1,
1513 number_of_returns: 1,
1514 synthetic: 0,
1515 key_point: 0,
1516 withheld: 0,
1517 overlap: 0,
1518 scan_channel: 0,
1519 scan_direction_flag: 0,
1520 edge_of_flight_line: 0,
1521 classification: 0,
1522 user_data: 0,
1523 scan_angle: 0.0,
1524 point_source_id: 0,
1525 gps_time: f64::from(i),
1526 red: 0,
1527 green: 0,
1528 blue: 0,
1529 })
1530 .collect();
1531 let source = VecSource { points };
1532 let bounds = source_bounds(&source);
1533 let (center, halfsize) = cube_from_bounds(&bounds);
1534 let params = CopcWriterParams {
1535 max_points_per_node: 7,
1536 max_depth: 5,
1537 };
1538
1539 let spooled = build_lod_index(&source, center, halfsize, ¶ms, &NeverCancel).unwrap();
1540 let ranges = read_lod_index(&spooled).unwrap();
1541
1542 let mut seen = vec![false; source.len()];
1543 let mut total = 0usize;
1544 for (key, indices) in ranges {
1545 if key.level < params.max_depth as i32 {
1546 assert!(indices.len() <= params.max_points_per_node as usize);
1547 }
1548 for index in indices {
1549 let seen = &mut seen[index as usize];
1550 assert!(!*seen, "point index {index} was assigned more than once");
1551 *seen = true;
1552 total += 1;
1553 }
1554 }
1555 assert_eq!(source.len(), total);
1556 assert!(seen.into_iter().all(|value| value));
1557 }
1558
1559 #[test]
1560 fn hierarchy_plan_splits_large_root_page() {
1561 let mut entries = vec![Entry {
1562 key: VoxelKey::root(),
1563 offset: 1,
1564 byte_size: 1,
1565 point_count: 1,
1566 }];
1567 let mut offset = 2;
1568 for z in 0..16 {
1569 for y in 0..16 {
1570 for x in 0..16 {
1571 entries.push(Entry {
1572 key: VoxelKey { level: 4, x, y, z },
1573 offset,
1574 byte_size: 1,
1575 point_count: 1,
1576 });
1577 offset += 1;
1578 }
1579 }
1580 }
1581 entries.sort_by_key(|entry| entry.key);
1582
1583 let mut plan = plan_hierarchy_pages(&entries, VoxelKey::root()).unwrap();
1584 let start = 1024;
1585 let end = assign_hierarchy_page_offsets(&mut plan, start).unwrap();
1586
1587 assert!(plan.byte_size < hierarchy_page_byte_size(entries.len()).unwrap());
1588 assert!(plan
1589 .items
1590 .iter()
1591 .any(|item| matches!(item, HierarchyPageItem::Child(_))));
1592
1593 let mut out = Cursor::new(vec![0; start as usize]);
1594 out.seek(SeekFrom::Start(start)).unwrap();
1595 write_hierarchy_page_tree(&mut out, &plan).unwrap();
1596 assert_eq!(end, out.get_ref().len() as u64);
1597 }
1598
1599 fn source_bounds(source: &VecSource) -> Bounds {
1600 source.points.iter().fold(
1601 Bounds::point(source.points[0].x, source.points[0].y, source.points[0].z),
1602 |mut bounds, point| {
1603 bounds.extend(point.x, point.y, point.z);
1604 bounds
1605 },
1606 )
1607 }
1608
1609 fn read_lod_index(index: &LodIndex) -> Result<Vec<(VoxelKey, Vec<u32>)>> {
1610 let path: &Path = index.order_path.as_ref();
1611 let mut reader =
1612 BufReader::new(File::open(path).map_err(|e| Error::io("open LOD order", e))?);
1613 let mut out = Vec::new();
1614 for node in &index.nodes {
1615 reader
1616 .seek(SeekFrom::Start(node.start))
1617 .map_err(|e| Error::io("seek LOD order", e))?;
1618 let mut indices = Vec::with_capacity(node.count);
1619 for _ in 0..node.count {
1620 indices.push(
1621 reader
1622 .read_u32::<LittleEndian>()
1623 .map_err(|e| Error::io("read LOD order", e))?,
1624 );
1625 }
1626 out.push((node.key, indices));
1627 }
1628 Ok(out)
1629 }
1630}