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
12pub struct CopcReader {
14 path: std::path::PathBuf,
15 metadata: SpatialMetadata,
16 schema: PointSchema,
17 file_info: CopcFileInfo,
18}
19
20impl CopcReader {
21 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 #[must_use]
36 pub fn file_info(&self) -> &CopcFileInfo {
37 &self.file_info
38 }
39
40 #[must_use]
42 pub fn root_bounds(&self) -> crate::copc::CopcBounds {
43 self.file_info.root_bounds
44 }
45
46 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
83pub 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
89pub fn read_copc(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
91 read_copc_file(path)
92}
93
94pub 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
100pub 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
108pub 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}