Skip to main content

copc_streaming/
header.rs

1//! LAS header + COPC info parsing.
2//!
3//! Uses the `las` crate for standard LAS/VLR parsing.
4//! Only the COPC info VLR (160 bytes) is parsed by us — it's COPC-specific.
5
6use std::io::Cursor;
7
8use byteorder::{LittleEndian, ReadBytesExt};
9use las::raw;
10use laz::LazVlr;
11
12use crate::error::CopcError;
13use crate::types::Aabb;
14
15/// Parsed COPC file header.
16///
17/// LAS-standard fields come from `las::Header`.
18/// COPC-specific fields are in `copc_info`.
19pub struct CopcHeader {
20    pub(crate) las_header: las::Header,
21    pub(crate) copc_info: CopcInfo,
22    pub(crate) laz_vlr: LazVlr,
23    pub(crate) evlr_offset: u64,
24    pub(crate) evlr_count: u32,
25}
26
27impl CopcHeader {
28    /// Full LAS header with transforms, bounds, point format, etc.
29    pub fn las_header(&self) -> &las::Header {
30        &self.las_header
31    }
32
33    /// COPC-specific info (octree center, halfsize, hierarchy location).
34    pub fn copc_info(&self) -> &CopcInfo {
35        &self.copc_info
36    }
37
38    /// LAZ decompression parameters.
39    pub fn laz_vlr(&self) -> &LazVlr {
40        &self.laz_vlr
41    }
42
43    /// File offset where EVLRs start.
44    pub fn evlr_offset(&self) -> u64 {
45        self.evlr_offset
46    }
47
48    /// Number of EVLRs.
49    pub fn evlr_count(&self) -> u32 {
50        self.evlr_count
51    }
52}
53
54/// COPC info VLR payload (160 bytes). This is COPC-specific — not part of the LAS standard.
55#[derive(Debug, Clone)]
56pub struct CopcInfo {
57    /// Centre of the root octree cube `[x, y, z]`.
58    pub center: [f64; 3],
59    /// Half the side length of the root octree cube.
60    pub halfsize: f64,
61    /// Spacing at the finest octree level.
62    pub spacing: f64,
63    /// File offset of the root hierarchy page.
64    pub root_hier_offset: u64,
65    /// Size of the root hierarchy page in bytes.
66    pub root_hier_size: u64,
67    /// Minimum GPS time across all points.
68    pub gpstime_minimum: f64,
69    /// Maximum GPS time across all points.
70    pub gpstime_maximum: f64,
71}
72
73impl CopcInfo {
74    /// Compute the root octree bounding box from center + halfsize.
75    pub fn root_bounds(&self) -> Aabb {
76        Aabb {
77            min: [
78                self.center[0] - self.halfsize,
79                self.center[1] - self.halfsize,
80                self.center[2] - self.halfsize,
81            ],
82            max: [
83                self.center[0] + self.halfsize,
84                self.center[1] + self.halfsize,
85                self.center[2] + self.halfsize,
86            ],
87        }
88    }
89
90    fn parse(data: &[u8]) -> Result<Self, CopcError> {
91        if data.len() < 160 {
92            return Err(CopcError::CopcInfoNotFound);
93        }
94        let mut r = Cursor::new(data);
95        let center_x = r.read_f64::<LittleEndian>()?;
96        let center_y = r.read_f64::<LittleEndian>()?;
97        let center_z = r.read_f64::<LittleEndian>()?;
98        let halfsize = r.read_f64::<LittleEndian>()?;
99        let spacing = r.read_f64::<LittleEndian>()?;
100        let root_hier_offset = r.read_u64::<LittleEndian>()?;
101        let root_hier_size = r.read_u64::<LittleEndian>()?;
102        let gpstime_minimum = r.read_f64::<LittleEndian>()?;
103        let gpstime_maximum = r.read_f64::<LittleEndian>()?;
104        Ok(CopcInfo {
105            center: [center_x, center_y, center_z],
106            halfsize,
107            spacing,
108            root_hier_offset,
109            root_hier_size,
110            gpstime_minimum,
111            gpstime_maximum,
112        })
113    }
114}
115
116/// Parse COPC header from a byte buffer.
117///
118/// The buffer must contain the LAS header and all VLRs (typically first ~64KB).
119pub(crate) fn parse_header(data: &[u8]) -> Result<CopcHeader, CopcError> {
120    let mut cursor = Cursor::new(data);
121
122    // Parse the raw LAS header
123    let raw_header = raw::Header::read_from(&mut cursor)?;
124
125    let evlr_offset = raw_header
126        .evlr
127        .as_ref()
128        .map_or(0, |e| e.start_of_first_evlr);
129    let evlr_count = raw_header.evlr.as_ref().map_or(0, |e| e.number_of_evlrs);
130    let number_of_vlrs = raw_header.number_of_variable_length_records;
131    let header_size = raw_header.header_size;
132
133    // Build the high-level header (for transforms, point format, etc.)
134    let mut builder = las::Builder::new(raw_header)?;
135
136    // Seek to VLR start and parse all VLRs
137    cursor.set_position(header_size as u64);
138
139    let mut copc_info = None;
140    let mut laz_vlr = None;
141
142    for _ in 0..number_of_vlrs {
143        let raw_vlr = raw::Vlr::read_from(&mut cursor, false)?;
144        let vlr = las::Vlr::new(raw_vlr);
145
146        match (vlr.user_id.as_str(), vlr.record_id) {
147            ("copc", 1) => {
148                copc_info = Some(CopcInfo::parse(&vlr.data)?);
149            }
150            ("laszip encoded", 22204) => {
151                laz_vlr = Some(LazVlr::read_from(vlr.data.as_slice())?);
152            }
153            _ => {}
154        }
155
156        builder.vlrs.push(vlr);
157    }
158
159    let las_header = builder.into_header()?;
160    let copc_info = copc_info.ok_or(CopcError::CopcInfoNotFound)?;
161    let laz_vlr = laz_vlr.ok_or(CopcError::LazVlrNotFound)?;
162
163    Ok(CopcHeader {
164        las_header,
165        copc_info,
166        laz_vlr,
167        evlr_offset,
168        evlr_count,
169    })
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_copc_info_root_bounds() {
178        let info = CopcInfo {
179            center: [100.0, 200.0, 10.0],
180            halfsize: 50.0,
181            spacing: 1.0,
182            root_hier_offset: 0,
183            root_hier_size: 0,
184            gpstime_minimum: 0.0,
185            gpstime_maximum: 0.0,
186        };
187        let b = info.root_bounds();
188        assert_eq!(b.min, [50.0, 150.0, -40.0]);
189        assert_eq!(b.max, [150.0, 250.0, 60.0]);
190    }
191}