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}