Skip to main content

copc_writer/
writer.rs

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/// Normalized point fields consumed by the COPC writer.
24#[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    /// Scan angle in degrees; encoded as LAS 1.4 scaled scan angle on write.
42    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
50/// Abstract point-data source for COPC emission.
51pub 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, &params, &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}