Skip to main content

spatialrust_io/copc/
reader.rs

1use std::path::Path;
2
3use copc_streaming::{ByteSource, CopcStreamingReader, FileSource};
4use las::Header;
5use spatialrust_core::{PointCloud, PointSchema, SpatialMetadata};
6
7use crate::copc::query::{CopcFileInfo, CopcQuery};
8use crate::error::{copc_parse, IoError};
9use crate::las::{metadata_from_las_header, point_cloud_from_las_points, schema_for_las_header};
10use crate::{PointReader, ReadOptions};
11
12/// Reads point clouds from COPC files.
13pub struct CopcReader {
14    path: std::path::PathBuf,
15    metadata: SpatialMetadata,
16    schema: PointSchema,
17    file_info: CopcFileInfo,
18}
19
20impl CopcReader {
21    /// Opens a COPC file and parses its header eagerly.
22    pub fn open(path: impl AsRef<Path>) -> Result<Self, IoError> {
23        let path = path.as_ref().to_path_buf();
24        let source = FileSource::open(&path).map_err(|error| copc_parse(error.to_string()))?;
25        let (header, file_info) = pollster::block_on(read_header_info(source))?;
26        Ok(Self {
27            schema: schema_for_las_header(&header),
28            metadata: metadata_from_las_header(),
29            file_info,
30            path,
31        })
32    }
33
34    /// Returns COPC header metadata parsed at open time.
35    #[must_use]
36    pub fn file_info(&self) -> &CopcFileInfo {
37        &self.file_info
38    }
39
40    /// Returns the root octree bounds for this file.
41    #[must_use]
42    pub fn root_bounds(&self) -> crate::copc::CopcBounds {
43        self.file_info.root_bounds
44    }
45
46    /// Reads points matching a spatial query.
47    pub fn read_query(&mut self, query: &CopcQuery) -> Result<PointCloud, IoError> {
48        read_copc_file_with_query(&self.path, query)
49    }
50}
51
52impl PointReader for CopcReader {
53    fn schema(&self) -> spatialrust_core::SpatialResult<PointSchema> {
54        Ok(self.schema.clone())
55    }
56
57    fn metadata(&self) -> spatialrust_core::SpatialResult<SpatialMetadata> {
58        Ok(self.metadata.clone())
59    }
60
61    fn read(&mut self, _options: &ReadOptions) -> spatialrust_core::SpatialResult<PointCloud> {
62        read_copc_file(&self.path)
63            .map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))
64    }
65}
66
67pub(crate) async fn read_header_info<S: ByteSource>(
68    source: S,
69) -> Result<(Header, CopcFileInfo), IoError> {
70    let reader =
71        CopcStreamingReader::open(source).await.map_err(|error| copc_parse(error.to_string()))?;
72    let las_header = reader.header().las_header().clone();
73    let copc_info = reader.copc_info();
74    let root = copc_info.root_bounds();
75    let file_info = CopcFileInfo {
76        root_bounds: crate::copc::CopcBounds::new(root.min, root.max),
77        spacing: copc_info.spacing,
78        point_count: las_header.number_of_points(),
79    };
80    Ok((las_header, file_info))
81}
82
83/// Reads COPC header metadata without loading points.
84pub fn read_copc_file_info(path: impl AsRef<Path>) -> Result<CopcFileInfo, IoError> {
85    let source = FileSource::open(path.as_ref()).map_err(|error| copc_parse(error.to_string()))?;
86    pollster::block_on(async { read_header_info(source).await.map(|(_, info)| info) })
87}
88
89/// Reads all points from a COPC file on disk.
90pub fn read_copc(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
91    read_copc_file(path)
92}
93
94/// Reads all points from a COPC file on disk.
95pub fn read_copc_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
96    let source = FileSource::open(path.as_ref()).map_err(|error| copc_parse(error.to_string()))?;
97    pollster::block_on(read_copc_from_byte_source(source, None))
98}
99
100/// Reads points inside a bounding box at full available detail.
101pub fn read_copc_file_in_bounds(
102    path: impl AsRef<Path>,
103    bounds: crate::copc::CopcBounds,
104) -> Result<PointCloud, IoError> {
105    read_copc_file_with_query(path, &CopcQuery::bounds(bounds))
106}
107
108/// Reads points using a spatial bounds and optional LOD limit.
109pub fn read_copc_file_with_query(
110    path: impl AsRef<Path>,
111    query: &CopcQuery,
112) -> Result<PointCloud, IoError> {
113    query.validate()?;
114    let source = FileSource::open(path.as_ref()).map_err(|error| copc_parse(error.to_string()))?;
115    pollster::block_on(read_copc_from_byte_source(source, Some(query)))
116}
117
118pub(crate) async fn read_copc_from_byte_source<S: ByteSource>(
119    source: S,
120    query: Option<&CopcQuery>,
121) -> Result<PointCloud, IoError> {
122    let mut reader =
123        CopcStreamingReader::open(source).await.map_err(|error| copc_parse(error.to_string()))?;
124
125    let las_header = reader.header().las_header().clone();
126    let schema = schema_for_las_header(&las_header);
127    let metadata = metadata_from_las_header();
128
129    let points = match query {
130        None => read_all_points(&mut reader).await?,
131        Some(query) => read_query_points(&mut reader, query).await?,
132    };
133
134    point_cloud_from_las_points(schema, metadata, points)
135}
136
137async fn read_all_points<S: ByteSource>(
138    reader: &mut CopcStreamingReader<S>,
139) -> Result<Vec<las::Point>, IoError> {
140    reader.load_all_hierarchy().await.map_err(|error| copc_parse(error.to_string()))?;
141
142    let mut points = Vec::new();
143    for (key, entry) in reader.entries() {
144        if entry.point_count == 0 {
145            continue;
146        }
147        let chunk = reader.fetch_chunk(key).await.map_err(|error| copc_parse(error.to_string()))?;
148        let chunk_points =
149            reader.read_points(&chunk).map_err(|error| copc_parse(error.to_string()))?;
150        points.extend(chunk_points);
151    }
152    Ok(points)
153}
154
155async fn read_query_points<S: ByteSource>(
156    reader: &mut CopcStreamingReader<S>,
157    query: &CopcQuery,
158) -> Result<Vec<las::Point>, IoError> {
159    let bounds = query.bounds.to_aabb();
160    if let Some(max_level) = query.max_level_for_spacing(reader.copc_info().spacing) {
161        reader
162            .query_points_to_level(&bounds, max_level)
163            .await
164            .map_err(|error| copc_parse(error.to_string()))
165    } else {
166        reader.query_points(&bounds).await.map_err(|error| copc_parse(error.to_string()))
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::{read_copc_file, read_copc_file_info, read_copc_file_with_query, CopcQuery};
173    use crate::copc::writer::write_copc_file;
174    use crate::copc::{copc_level_for_resolution, CopcBounds};
175    use crate::{write_las_file, LasWriteFormat};
176    use spatialrust_core::PointCloudBuilder;
177
178    #[test]
179    fn rejects_non_copc_laz() {
180        let mut builder = PointCloudBuilder::xyz();
181        builder.push_point([1.0, 2.0, 3.0]).unwrap();
182        let cloud = builder.build().unwrap();
183
184        let path = std::env::temp_dir().join(format!("spatialrust_laz_{}.laz", std::process::id()));
185        write_las_file(&path, &cloud, LasWriteFormat::Laz).unwrap();
186
187        let error = read_copc_file(&path).unwrap_err();
188        let _ = std::fs::remove_file(path);
189        assert!(matches!(error, crate::IoError::CopcParse(_)));
190    }
191
192    #[test]
193    fn rejects_invalid_query_bounds() {
194        let path = std::env::temp_dir()
195            .join(format!("spatialrust_copc_query_{}.copc.laz", std::process::id()));
196        let query = CopcQuery::bounds(CopcBounds::from_ranges((1.0, 0.0), (0.0, 1.0), (0.0, 1.0)));
197        let error = read_copc_file_with_query(&path, &query).unwrap_err();
198        assert!(matches!(error, crate::IoError::CopcFormat(_)));
199    }
200
201    #[test]
202    fn write_copc_rejects_empty_cloud() {
203        use spatialrust_core::{
204            PointBuffer, PointBufferSet, PointCloud, SpatialMetadata, StandardSchemas,
205        };
206
207        let schema = StandardSchemas::point_xyz();
208        let mut buffers = PointBufferSet::new();
209        for field in schema.fields() {
210            buffers.insert(field.name.clone(), PointBuffer::with_capacity(field.dtype, 0));
211        }
212        let cloud =
213            PointCloud::try_from_parts(schema, buffers, SpatialMetadata::default()).unwrap();
214        assert!(cloud.is_empty());
215
216        let path =
217            std::env::temp_dir().join(format!("spatialrust_copc_{}.copc.laz", std::process::id()));
218        let error = write_copc_file(&path, &cloud).unwrap_err();
219        assert!(matches!(error, crate::IoError::CopcFormat(_)));
220    }
221
222    #[test]
223    fn resolution_level_helper_is_usable_from_reader_tests() {
224        assert_eq!(copc_level_for_resolution(4.0, 1.0), 2);
225    }
226
227    #[test]
228    fn multi_resolution_copc_resolution_query_reduces_point_count() {
229        use copc_writer::CopcWriterParams;
230
231        use crate::copc::writer::write_copc_file_with_params;
232
233        let cloud = dense_grid_cloud(7_000);
234        let path = std::env::temp_dir()
235            .join(format!("spatialrust_copc_multires_{}.copc.laz", std::process::id()));
236        write_copc_file_with_params(
237            &path,
238            &cloud,
239            &CopcWriterParams { max_points_per_node: 96, max_depth: 8 },
240        )
241        .unwrap();
242
243        let info = read_copc_file_info(&path).unwrap();
244        let full = read_copc_file(&path).unwrap();
245        assert_eq!(full.len(), cloud.len());
246
247        let coarse = read_copc_file_with_query(
248            &path,
249            &CopcQuery::with_resolution(info.root_bounds, info.spacing * 4.0),
250        )
251        .unwrap();
252        let medium = read_copc_file_with_query(
253            &path,
254            &CopcQuery::with_resolution(info.root_bounds, info.spacing),
255        )
256        .unwrap();
257        let fine = read_copc_file_with_query(
258            &path,
259            &CopcQuery::with_resolution(info.root_bounds, info.spacing / 4.0),
260        )
261        .unwrap();
262
263        assert!(coarse.len() <= medium.len());
264        assert!(medium.len() <= fine.len());
265        assert!(fine.len() <= full.len());
266        assert!(
267            coarse.len() < full.len(),
268            "coarse resolution should load fewer points than full detail"
269        );
270
271        let level0 =
272            read_copc_file_with_query(&path, &CopcQuery::with_level(info.root_bounds, 0)).unwrap();
273        let level2 =
274            read_copc_file_with_query(&path, &CopcQuery::with_level(info.root_bounds, 2)).unwrap();
275        assert!(level0.len() <= level2.len());
276        assert!(level2.len() <= full.len());
277
278        let _ = std::fs::remove_file(path);
279    }
280
281    fn dense_grid_cloud(count: usize) -> spatialrust_core::PointCloud {
282        use spatialrust_core::PointCloudBuilder;
283
284        let mut builder = PointCloudBuilder::xyz();
285        for index in 0..count {
286            let x = (index % 31) as f32 - 15.0;
287            let y = ((index / 31) % 29) as f32 - 14.0;
288            let z = ((index / (31 * 29)) % 23) as f32 - 11.0;
289            builder.push_point([x, y, z]).unwrap();
290        }
291        builder.build().unwrap()
292    }
293}