Skip to main content

copc_streaming/
reader.rs

1//! High-level streaming COPC reader.
2
3use crate::byte_source::ByteSource;
4use crate::chunk::{self, Chunk};
5use crate::error::CopcError;
6use crate::fields::Fields;
7use crate::header::{self, CopcHeader, CopcInfo};
8use crate::hierarchy::{HierarchyCache, HierarchyEntry};
9use crate::types::VoxelKey;
10
11/// Async streaming COPC reader.
12///
13/// `open()` reads the LAS header, VLRs, and root hierarchy page.
14/// Deeper hierarchy pages and point chunks are loaded on demand.
15pub struct CopcStreamingReader<S: ByteSource> {
16    source: S,
17    header: CopcHeader,
18    hierarchy: HierarchyCache,
19}
20
21impl<S: ByteSource> CopcStreamingReader<S> {
22    /// Open a COPC file.
23    pub async fn open(source: S) -> Result<Self, CopcError> {
24        let size = source.size().await?.unwrap_or(65536);
25        let read_size = size.min(65536);
26        let data = source.read_range(0, read_size).await?;
27        let header = header::parse_header(&data)?;
28
29        let mut hierarchy = HierarchyCache::new();
30        hierarchy.load_root(&source, &header.copc_info).await?;
31
32        Ok(Self {
33            source,
34            header,
35            hierarchy,
36        })
37    }
38
39    // --- Header accessors ---
40
41    /// The parsed COPC file header.
42    pub fn header(&self) -> &CopcHeader {
43        &self.header
44    }
45
46    /// Shortcut for `header().copc_info()`.
47    pub fn copc_info(&self) -> &CopcInfo {
48        &self.header.copc_info
49    }
50
51    /// File offset where EVLRs start.
52    pub fn evlr_offset(&self) -> u64 {
53        self.header.evlr_offset
54    }
55
56    /// Number of EVLRs in the file.
57    pub fn evlr_count(&self) -> u32 {
58        self.header.evlr_count
59    }
60
61    /// The underlying byte source.
62    pub fn source(&self) -> &S {
63        &self.source
64    }
65
66    // --- Hierarchy queries ---
67
68    /// Look up a hierarchy entry by voxel key.
69    pub fn get(&self, key: &VoxelKey) -> Option<&HierarchyEntry> {
70        self.hierarchy.get(key)
71    }
72
73    /// Iterate all loaded hierarchy entries.
74    pub fn entries(&self) -> impl Iterator<Item = (&VoxelKey, &HierarchyEntry)> {
75        self.hierarchy.iter()
76    }
77
78    /// Return loaded child entries for a given node.
79    ///
80    /// Only returns children that are already in the hierarchy cache.
81    /// If deeper hierarchy pages haven't been loaded yet, this may
82    /// return fewer children than actually exist in the file.
83    pub fn children(&self, key: &VoxelKey) -> Vec<&HierarchyEntry> {
84        key.children()
85            .iter()
86            .filter_map(|child| self.hierarchy.get(child))
87            .collect()
88    }
89
90    /// Number of loaded hierarchy entries.
91    pub fn node_count(&self) -> usize {
92        self.hierarchy.len()
93    }
94
95    /// Whether there are hierarchy pages that haven't been loaded yet.
96    pub fn has_pending_pages(&self) -> bool {
97        self.hierarchy.has_pending_pages()
98    }
99
100    /// Iterate the [`VoxelKey`]s anchoring the currently-pending hierarchy
101    /// pages. See [`HierarchyCache::pending_page_keys`](crate::HierarchyCache::pending_page_keys).
102    pub fn pending_page_keys(&self) -> impl Iterator<Item = VoxelKey> + '_ {
103        self.hierarchy.pending_page_keys()
104    }
105
106    // --- Hierarchy loading ---
107
108    /// Load the next batch of pending hierarchy pages.
109    pub async fn load_pending_pages(&mut self) -> Result<(), CopcError> {
110        self.hierarchy.load_pending_pages(&self.source).await
111    }
112
113    /// Load only hierarchy pages whose subtree intersects `bounds`.
114    ///
115    /// Pages outside the region are left pending for future calls.
116    /// Much cheaper than `load_all_hierarchy` when querying a small area.
117    pub async fn load_hierarchy_for_bounds(
118        &mut self,
119        bounds: &crate::types::Aabb,
120    ) -> Result<(), CopcError> {
121        let root_bounds = self.header.copc_info.root_bounds();
122        self.hierarchy
123            .load_pages_for_bounds(&self.source, bounds, &root_bounds)
124            .await
125    }
126
127    /// Load hierarchy pages intersecting `bounds`, down to `max_level`.
128    ///
129    /// Pages deeper than `max_level` are left pending even if they overlap
130    /// the bounds. Combine with [`CopcInfo::level_for_resolution`] to load
131    /// only the detail you need:
132    ///
133    /// ```rust,ignore
134    /// let level = reader.copc_info().level_for_resolution(0.5);
135    /// reader.load_hierarchy_for_bounds_to_level(&camera_box, level).await?;
136    /// ```
137    pub async fn load_hierarchy_for_bounds_to_level(
138        &mut self,
139        bounds: &crate::types::Aabb,
140        max_level: i32,
141    ) -> Result<(), CopcError> {
142        let root_bounds = self.header.copc_info.root_bounds();
143        self.hierarchy
144            .load_pages_for_bounds_to_level(&self.source, bounds, &root_bounds, max_level)
145            .await
146    }
147
148    /// Load all remaining hierarchy pages.
149    pub async fn load_all_hierarchy(&mut self) -> Result<(), CopcError> {
150        self.hierarchy
151            .load_all(&self.source, &self.header.copc_info)
152            .await
153    }
154
155    // ==================== Fast API (tier 2) ====================
156    //
157    // Returns `Chunk`s with zero-copy column access. Caller picks which
158    // fields to decode, walks columns directly, and decides when to
159    // materialize `las::Point` values (if ever).
160
161    /// Fetch and decompress a single chunk, decoding only the fields in
162    /// `fields`.
163    ///
164    /// On LAS 1.4 layered formats (6/7/8 — the formats COPC mandates) this
165    /// is a real CPU saving proportional to the number of skipped layers.
166    /// Fields listed in [`Fields`] map directly to LAZ layer-level
167    /// `DecompressionSelection` and the omitted layers are not arithmetically
168    /// decoded at all.
169    pub async fn fetch_chunk(&self, key: &VoxelKey, fields: Fields) -> Result<Chunk, CopcError> {
170        self.fetch_chunk_with_source(&self.source, key, fields)
171            .await
172    }
173
174    /// Fetch and decompress a point chunk using an external byte source.
175    ///
176    /// This is useful when the reader is behind a lock and you want to
177    /// extract the metadata under the lock, then do the async fetch
178    /// without holding it.
179    pub async fn fetch_chunk_with_source(
180        &self,
181        source: &impl ByteSource,
182        key: &VoxelKey,
183        fields: Fields,
184    ) -> Result<Chunk, CopcError> {
185        let entry = self
186            .hierarchy
187            .get(key)
188            .ok_or(CopcError::NodeNotFound(*key))?;
189        chunk::fetch_and_decompress(
190            source,
191            entry,
192            &self.header.laz_vlr,
193            &self.header.las_header,
194            fields,
195        )
196        .await
197    }
198
199    /// Fetch and decompress multiple chunks in one batched I/O call.
200    ///
201    /// Uses [`ByteSource::read_ranges`] to coalesce the fetches — HTTP
202    /// sources that override `read_ranges` with parallel requests or
203    /// range merging will issue far fewer round-trips than calling
204    /// [`fetch_chunk`](Self::fetch_chunk) in a loop.
205    ///
206    /// All keys must already be present in the loaded hierarchy; call
207    /// one of the `load_hierarchy_*` methods first.
208    pub async fn fetch_chunks(
209        &self,
210        keys: &[VoxelKey],
211        fields: Fields,
212    ) -> Result<Vec<Chunk>, CopcError> {
213        let entries: Vec<&HierarchyEntry> = keys
214            .iter()
215            .map(|k| self.hierarchy.get(k).ok_or(CopcError::NodeNotFound(*k)))
216            .collect::<Result<_, _>>()?;
217
218        let ranges: Vec<(u64, u64)> = entries
219            .iter()
220            .map(|e| (e.offset, e.byte_size as u64))
221            .collect();
222
223        let compressed_blobs = self.source.read_ranges(&ranges).await?;
224
225        compressed_blobs
226            .iter()
227            .zip(entries.iter())
228            .map(|(data, entry)| {
229                chunk::decompress_chunk(
230                    data,
231                    entry,
232                    &self.header.laz_vlr,
233                    &self.header.las_header,
234                    fields,
235                )
236            })
237            .collect()
238    }
239
240    /// Keys in the currently-loaded hierarchy whose subtree intersects
241    /// `bounds` and whose level is at most `max_level` (if provided).
242    ///
243    /// Use this to drive your own fetch loop — for parallelism, cancellation,
244    /// prioritization, or streaming:
245    ///
246    /// ```rust,ignore
247    /// reader.load_hierarchy_for_bounds_to_level(&bbox, lod).await?;
248    /// for key in reader.visible_keys(&bbox, Some(lod)) {
249    ///     let chunk = reader.fetch_chunk(&key, Fields::Z | Fields::RGB).await?;
250    ///     // ...
251    /// }
252    /// ```
253    pub fn visible_keys(
254        &self,
255        bounds: &crate::types::Aabb,
256        max_level: Option<i32>,
257    ) -> Vec<VoxelKey> {
258        let root_bounds = self.header.copc_info.root_bounds();
259        self.hierarchy
260            .iter()
261            .filter(|(k, e)| {
262                e.point_count > 0
263                    && max_level.is_none_or(|lvl| k.level <= lvl)
264                    && k.bounds(&root_bounds).intersects(bounds)
265            })
266            .map(|(k, _)| *k)
267            .collect()
268    }
269
270    /// Load hierarchy for `bounds`, fetch every intersecting chunk, and
271    /// return them.
272    ///
273    /// Uses [`fetch_chunks`](Self::fetch_chunks) to batch all chunk
274    /// fetches into a single [`ByteSource::read_ranges`] call.
275    pub async fn query_chunks(
276        &mut self,
277        bounds: &crate::types::Aabb,
278        fields: Fields,
279    ) -> Result<Vec<Chunk>, CopcError> {
280        self.load_hierarchy_for_bounds(bounds).await?;
281        let keys = self.visible_keys(bounds, None);
282        self.fetch_chunks(&keys, fields).await
283    }
284
285    /// Same as [`query_chunks`](Self::query_chunks) but limits the octree
286    /// depth to `max_level`.
287    pub async fn query_chunks_to_level(
288        &mut self,
289        bounds: &crate::types::Aabb,
290        max_level: i32,
291        fields: Fields,
292    ) -> Result<Vec<Chunk>, CopcError> {
293        self.load_hierarchy_for_bounds_to_level(bounds, max_level)
294            .await?;
295        let keys = self.visible_keys(bounds, Some(max_level));
296        self.fetch_chunks(&keys, fields).await
297    }
298
299    // ==================== Simple API (tier 1) ====================
300    //
301    // One-line entry points for scripts, tests, and prototypes. Always
302    // decodes every field and materializes `las::Point` values. Thin
303    // wrappers over the fast API — no duplicate read paths.
304
305    /// Fetch, decompress, and materialize every point in one chunk as
306    /// owned [`las::Point`] values.
307    ///
308    /// Equivalent to `fetch_chunk(key, Fields::ALL).await?.to_points()?`.
309    /// Prefer [`fetch_chunk`](Self::fetch_chunk) for performance-sensitive
310    /// code paths.
311    pub async fn fetch_points(&self, key: &VoxelKey) -> Result<Vec<las::Point>, CopcError> {
312        self.fetch_chunk(key, Fields::ALL).await?.to_points()
313    }
314
315    /// Load hierarchy for `bounds`, fetch all intersecting chunks, and
316    /// return the points inside `bounds`.
317    ///
318    /// Thin wrapper over [`query_chunks`](Self::query_chunks) with
319    /// `Fields::ALL`. Decodes everything and materializes; prefer
320    /// `query_chunks` when you only need a subset of fields or want to
321    /// walk points column-by-column.
322    pub async fn query_points(
323        &mut self,
324        bounds: &crate::types::Aabb,
325    ) -> Result<Vec<las::Point>, CopcError> {
326        let chunks = self.query_chunks(bounds, Fields::ALL).await?;
327        let mut out = Vec::new();
328        for chunk in &chunks {
329            // `indices_in_bounds` on an `ALL`-decoded chunk always returns Some.
330            let indices = chunk
331                .indices_in_bounds(bounds)
332                .expect("Fields::ALL includes Fields::Z");
333            if indices.is_empty() {
334                continue;
335            }
336            out.extend(chunk.points_at(&indices)?);
337        }
338        Ok(out)
339    }
340
341    /// Load hierarchy to `max_level` and return all points inside `bounds`.
342    ///
343    /// Like [`query_points`](Self::query_points) but limits the octree depth.
344    /// Use with [`CopcInfo::level_for_resolution`] for LOD control:
345    ///
346    /// ```rust,ignore
347    /// let level = reader.copc_info().level_for_resolution(0.5);
348    /// let points = reader.query_points_to_level(&visible_box, level).await?;
349    /// ```
350    pub async fn query_points_to_level(
351        &mut self,
352        bounds: &crate::types::Aabb,
353        max_level: i32,
354    ) -> Result<Vec<las::Point>, CopcError> {
355        let chunks = self
356            .query_chunks_to_level(bounds, max_level, Fields::ALL)
357            .await?;
358        let mut out = Vec::new();
359        for chunk in &chunks {
360            let indices = chunk
361                .indices_in_bounds(bounds)
362                .expect("Fields::ALL includes Fields::Z");
363            if indices.is_empty() {
364                continue;
365            }
366            out.extend(chunk.points_at(&indices)?);
367        }
368        Ok(out)
369    }
370}