Skip to main content

copc_streaming/
reader.rs

1//! High-level streaming COPC reader.
2
3use crate::byte_source::ByteSource;
4use crate::chunk::{self, DecompressedChunk};
5use crate::error::CopcError;
6use crate::header::{self, CopcHeader, CopcInfo};
7use crate::hierarchy::{HierarchyCache, HierarchyEntry};
8use crate::types::VoxelKey;
9
10/// Async streaming COPC reader.
11///
12/// `open()` reads the LAS header, VLRs, and root hierarchy page.
13/// Deeper hierarchy pages and point chunks are loaded on demand.
14pub struct CopcStreamingReader<S: ByteSource> {
15    source: S,
16    header: CopcHeader,
17    hierarchy: HierarchyCache,
18}
19
20impl<S: ByteSource> CopcStreamingReader<S> {
21    /// Open a COPC file.
22    pub async fn open(source: S) -> Result<Self, CopcError> {
23        let size = source.size().await?.unwrap_or(65536);
24        let read_size = size.min(65536);
25        let data = source.read_range(0, read_size).await?;
26        let header = header::parse_header(&data)?;
27
28        let mut hierarchy = HierarchyCache::new();
29        hierarchy.load_root(&source, &header.copc_info).await?;
30
31        Ok(Self {
32            source,
33            header,
34            hierarchy,
35        })
36    }
37
38    // --- Header accessors ---
39
40    /// The parsed COPC file header.
41    pub fn header(&self) -> &CopcHeader {
42        &self.header
43    }
44
45    /// Shortcut for `header().copc_info()`.
46    pub fn copc_info(&self) -> &CopcInfo {
47        &self.header.copc_info
48    }
49
50    /// File offset where EVLRs start.
51    pub fn evlr_offset(&self) -> u64 {
52        self.header.evlr_offset
53    }
54
55    /// Number of EVLRs in the file.
56    pub fn evlr_count(&self) -> u32 {
57        self.header.evlr_count
58    }
59
60    /// The underlying byte source.
61    pub fn source(&self) -> &S {
62        &self.source
63    }
64
65    // --- Hierarchy queries ---
66
67    /// Look up a hierarchy entry by voxel key.
68    pub fn get(&self, key: &VoxelKey) -> Option<&HierarchyEntry> {
69        self.hierarchy.get(key)
70    }
71
72    /// Iterate all loaded hierarchy entries.
73    pub fn entries(&self) -> impl Iterator<Item = (&VoxelKey, &HierarchyEntry)> {
74        self.hierarchy.iter()
75    }
76
77    /// Return loaded child entries for a given node.
78    ///
79    /// Only returns children that are already in the hierarchy cache.
80    /// If deeper hierarchy pages haven't been loaded yet, this may
81    /// return fewer children than actually exist in the file.
82    pub fn children(&self, key: &VoxelKey) -> Vec<&HierarchyEntry> {
83        key.children()
84            .iter()
85            .filter_map(|child| self.hierarchy.get(child))
86            .collect()
87    }
88
89    /// Number of loaded hierarchy entries.
90    pub fn node_count(&self) -> usize {
91        self.hierarchy.len()
92    }
93
94    /// Whether there are hierarchy pages that haven't been loaded yet.
95    pub fn has_pending_pages(&self) -> bool {
96        self.hierarchy.has_pending_pages()
97    }
98
99    // --- Hierarchy loading ---
100
101    /// Load the next batch of pending hierarchy pages.
102    pub async fn load_pending_pages(&mut self) -> Result<(), CopcError> {
103        self.hierarchy.load_pending_pages(&self.source).await
104    }
105
106    /// Load only hierarchy pages whose subtree intersects `bounds`.
107    ///
108    /// Pages outside the region are left pending for future calls.
109    /// Much cheaper than `load_all_hierarchy` when querying a small area.
110    pub async fn load_hierarchy_for_bounds(
111        &mut self,
112        bounds: &crate::types::Aabb,
113    ) -> Result<(), CopcError> {
114        let root_bounds = self.header.copc_info.root_bounds();
115        self.hierarchy
116            .load_pages_for_bounds(&self.source, bounds, &root_bounds)
117            .await
118    }
119
120    /// Load hierarchy pages intersecting `bounds`, down to `max_level`.
121    ///
122    /// Pages deeper than `max_level` are left pending even if they overlap
123    /// the bounds. Combine with [`CopcInfo::level_for_resolution`] to load
124    /// only the detail you need:
125    ///
126    /// ```rust,ignore
127    /// let level = reader.copc_info().level_for_resolution(0.5);
128    /// reader.load_hierarchy_for_bounds_to_level(&camera_box, level).await?;
129    /// ```
130    pub async fn load_hierarchy_for_bounds_to_level(
131        &mut self,
132        bounds: &crate::types::Aabb,
133        max_level: i32,
134    ) -> Result<(), CopcError> {
135        let root_bounds = self.header.copc_info.root_bounds();
136        self.hierarchy
137            .load_pages_for_bounds_to_level(&self.source, bounds, &root_bounds, max_level)
138            .await
139    }
140
141    /// Load all remaining hierarchy pages.
142    pub async fn load_all_hierarchy(&mut self) -> Result<(), CopcError> {
143        self.hierarchy
144            .load_all(&self.source, &self.header.copc_info)
145            .await
146    }
147
148    // --- Point data ---
149
150    /// Fetch and decompress a single point chunk.
151    pub async fn fetch_chunk(&self, key: &VoxelKey) -> Result<DecompressedChunk, CopcError> {
152        self.fetch_chunk_with_source(&self.source, key).await
153    }
154
155    /// Fetch and decompress a point chunk using an external byte source.
156    ///
157    /// This is useful when the reader is behind a lock and you want to
158    /// extract the metadata under the lock, then do the async fetch
159    /// without holding it.
160    pub async fn fetch_chunk_with_source(
161        &self,
162        source: &impl ByteSource,
163        key: &VoxelKey,
164    ) -> Result<DecompressedChunk, CopcError> {
165        let entry = self
166            .hierarchy
167            .get(key)
168            .ok_or(CopcError::NodeNotFound(*key))?;
169        let point_record_length = self.header.las_header.point_format().len()
170            + self.header.las_header.point_format().extra_bytes;
171        chunk::fetch_and_decompress(source, entry, &self.header.laz_vlr, point_record_length).await
172    }
173
174    /// Parse all points from a decompressed chunk.
175    pub fn read_points(&self, chunk: &DecompressedChunk) -> Result<Vec<las::Point>, CopcError> {
176        chunk::read_points(chunk, &self.header.las_header)
177    }
178
179    /// Parse a sub-range of points from a decompressed chunk.
180    ///
181    /// Only the points in `range` are parsed — bytes outside the range are skipped.
182    /// Pair with `NodeTemporalEntry::estimate_point_range` from the `copc-temporal`
183    /// crate to read only the points that fall within a time window.
184    pub fn read_points_range(
185        &self,
186        chunk: &DecompressedChunk,
187        range: std::ops::Range<u32>,
188    ) -> Result<Vec<las::Point>, CopcError> {
189        chunk::read_points_range(chunk, &self.header.las_header, range)
190    }
191
192    /// Parse all points from a chunk, keeping only those inside `bounds`.
193    pub fn read_points_in_bounds(
194        &self,
195        chunk: &DecompressedChunk,
196        bounds: &crate::types::Aabb,
197    ) -> Result<Vec<las::Point>, CopcError> {
198        let points = chunk::read_points(chunk, &self.header.las_header)?;
199        Ok(filter_points_by_bounds(points, bounds))
200    }
201
202    /// Parse a sub-range of points, keeping only those inside `bounds`.
203    ///
204    /// Combines temporal range estimation with spatial filtering: first only
205    /// the points in `range` are decompressed, then points outside `bounds`
206    /// are discarded.
207    pub fn read_points_range_in_bounds(
208        &self,
209        chunk: &DecompressedChunk,
210        range: std::ops::Range<u32>,
211        bounds: &crate::types::Aabb,
212    ) -> Result<Vec<las::Point>, CopcError> {
213        let points = chunk::read_points_range(chunk, &self.header.las_header, range)?;
214        Ok(filter_points_by_bounds(points, bounds))
215    }
216
217    // --- High-level queries ---
218
219    /// Load hierarchy and return all points inside `bounds`.
220    ///
221    /// This is the simplest way to query a spatial region. It loads the
222    /// hierarchy pages that overlap `bounds`, fetches and decompresses
223    /// matching chunks, and returns only the points inside the bounding box.
224    ///
225    /// ```rust,ignore
226    /// let points = reader.query_points(&my_query_box).await?;
227    /// ```
228    pub async fn query_points(
229        &mut self,
230        bounds: &crate::types::Aabb,
231    ) -> Result<Vec<las::Point>, CopcError> {
232        self.load_hierarchy_for_bounds(bounds).await?;
233        let root_bounds = self.header.copc_info.root_bounds();
234
235        let keys: Vec<VoxelKey> = self
236            .hierarchy
237            .iter()
238            .filter(|(k, e)| e.point_count > 0 && k.bounds(&root_bounds).intersects(bounds))
239            .map(|(k, _)| *k)
240            .collect();
241
242        let mut all_points = Vec::new();
243        for key in keys {
244            let chunk = self.fetch_chunk(&key).await?;
245            let points = self.read_points_in_bounds(&chunk, bounds)?;
246            all_points.extend(points);
247        }
248        Ok(all_points)
249    }
250
251    /// Load hierarchy to `max_level` and return all points inside `bounds`.
252    ///
253    /// Like [`query_points`](Self::query_points) but limits the octree depth.
254    /// Use with [`CopcInfo::level_for_resolution`] for LOD control:
255    ///
256    /// ```rust,ignore
257    /// let level = reader.copc_info().level_for_resolution(0.5);
258    /// let points = reader.query_points_to_level(&visible_box, level).await?;
259    /// ```
260    pub async fn query_points_to_level(
261        &mut self,
262        bounds: &crate::types::Aabb,
263        max_level: i32,
264    ) -> Result<Vec<las::Point>, CopcError> {
265        self.load_hierarchy_for_bounds_to_level(bounds, max_level)
266            .await?;
267        let root_bounds = self.header.copc_info.root_bounds();
268
269        let keys: Vec<VoxelKey> = self
270            .hierarchy
271            .iter()
272            .filter(|(k, e)| {
273                e.point_count > 0
274                    && k.level <= max_level
275                    && k.bounds(&root_bounds).intersects(bounds)
276            })
277            .map(|(k, _)| *k)
278            .collect();
279
280        let mut all_points = Vec::new();
281        for key in keys {
282            let chunk = self.fetch_chunk(&key).await?;
283            let points = self.read_points_in_bounds(&chunk, bounds)?;
284            all_points.extend(points);
285        }
286        Ok(all_points)
287    }
288}
289
290/// Filter points to only those inside an axis-aligned bounding box.
291pub fn filter_points_by_bounds(
292    points: Vec<las::Point>,
293    bounds: &crate::types::Aabb,
294) -> Vec<las::Point> {
295    points
296        .into_iter()
297        .filter(|p| {
298            p.x >= bounds.min[0]
299                && p.x <= bounds.max[0]
300                && p.y >= bounds.min[1]
301                && p.y <= bounds.max[1]
302                && p.z >= bounds.min[2]
303                && p.z <= bounds.max[2]
304        })
305        .collect()
306}