1use std::io::{BufRead, Seek};
2use std::path::Path;
3
4use las::{Header, Point, Reader};
5use spatialrust_core::{
6 DType, FieldSemantic, PointBuffer, PointBufferSet, PointCloud, PointSchema, SpatialMetadata,
7};
8
9use crate::error::{las_parse, IoError};
10use crate::las::schema::schema_for_las_header;
11use crate::{PointReader, ReadOptions};
12
13#[cfg(feature = "streaming")]
14use crate::streaming::{records_io, FormatStreamState};
15#[cfg(feature = "streaming")]
16use spatialrust_records::{
17 BoundedSpatialRecordSource, CancellationToken, MemoryTracker, RecordsResult, SchemaDescriptor,
18 SpatialRecordChunk, StreamOptions,
19};
20
21pub struct LasReader {
23 reader: Reader,
24 metadata: SpatialMetadata,
25 schema: PointSchema,
26 loaded: bool,
27}
28
29impl LasReader {
30 pub fn open(path: impl AsRef<Path>) -> Result<Self, IoError> {
32 let reader = Reader::from_path(path).map_err(|error| las_parse(error.to_string()))?;
33 let header = reader.header();
34 Ok(Self {
35 schema: schema_for_las_header(header),
36 metadata: metadata_from_header(header),
37 reader,
38 loaded: false,
39 })
40 }
41
42 #[must_use]
44 pub fn header(&self) -> &Header {
45 self.reader.header()
46 }
47
48 pub fn read_cloud(&mut self) -> Result<PointCloud, IoError> {
50 if self.loaded {
51 return Err(crate::error::las_format("LAS reader already consumed"));
52 }
53 self.loaded = true;
54 read_points_from_reader(&mut self.reader, self.schema.clone(), self.metadata.clone())
55 }
56}
57
58impl PointReader for LasReader {
59 fn schema(&self) -> spatialrust_core::SpatialResult<PointSchema> {
60 Ok(self.schema.clone())
61 }
62
63 fn metadata(&self) -> spatialrust_core::SpatialResult<SpatialMetadata> {
64 Ok(self.metadata.clone())
65 }
66
67 fn read(&mut self, _options: &ReadOptions) -> spatialrust_core::SpatialResult<PointCloud> {
68 self.read_cloud().map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))
69 }
70}
71
72pub fn read_las<R: BufRead + Seek + Send + Sync + 'static>(
74 reader: R,
75) -> Result<PointCloud, IoError> {
76 let mut las_reader = Reader::new(reader).map_err(|error| las_parse(error.to_string()))?;
77 let header = las_reader.header().clone();
78 let schema = schema_for_las_header(&header);
79 let metadata = metadata_from_header(&header);
80 read_points_from_reader(&mut las_reader, schema, metadata)
81}
82
83pub fn read_las_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
85 let mut reader = LasReader::open(path)?;
86 reader.read_cloud()
87}
88
89#[cfg(feature = "streaming")]
91pub struct LasChunkSource {
92 reader: Reader,
93 metadata: SpatialMetadata,
94 state: FormatStreamState,
95 finished: bool,
96}
97
98#[cfg(feature = "streaming")]
99impl LasChunkSource {
100 pub fn open(
102 path: impl AsRef<Path>,
103 options: StreamOptions,
104 cancellation: CancellationToken,
105 ) -> Result<Self, IoError> {
106 let reader = Reader::from_path(path).map_err(|error| las_parse(error.to_string()))?;
107 let schema = schema_for_las_header(reader.header());
108 let metadata = metadata_from_header(reader.header());
109 let state = FormatStreamState::new("las", schema, options, cancellation)?;
110 Ok(Self { reader, metadata, state, finished: false })
111 }
112
113 #[must_use]
115 pub fn header(&self) -> &Header {
116 self.reader.header()
117 }
118}
119
120#[cfg(feature = "streaming")]
121impl BoundedSpatialRecordSource for LasChunkSource {
122 fn schema(&self) -> &SchemaDescriptor {
123 &self.state.schema
124 }
125
126 fn options(&self) -> &StreamOptions {
127 &self.state.options
128 }
129
130 fn memory_tracker(&self) -> &MemoryTracker {
131 &self.state.tracker
132 }
133
134 fn cancellation_token(&self) -> CancellationToken {
135 self.state.cancellation.clone()
136 }
137
138 fn max_chunk_bytes(&self) -> u64 {
139 self.state.max_chunk_bytes
140 }
141
142 fn next_chunk(&mut self) -> Option<RecordsResult<SpatialRecordChunk>> {
143 if self.finished {
144 return None;
145 }
146 let reservation = match self.state.reserve_points(self.state.options.chunk_points()) {
147 Ok(reservation) => reservation,
148 Err(error) => return Some(Err(error)),
149 };
150 let schema = self.state.schema.point_schema().clone();
151 let mut buffers = PointBufferSet::new();
152 for field in schema.fields() {
153 buffers.insert(
154 field.name.clone(),
155 PointBuffer::with_capacity(field.dtype, self.state.options.chunk_points()),
156 );
157 }
158 let mut point_count = 0_usize;
159 while point_count < self.state.options.chunk_points() {
160 match self.reader.read_point() {
161 Ok(Some(point)) => {
162 if let Err(error) = append_las_point(&schema, &mut buffers, &point) {
163 return Some(Err(records_io(error)));
164 }
165 point_count += 1;
166 }
167 Err(error) => return Some(Err(records_io(las_parse(error.to_string())))),
168 Ok(None) => {
169 self.finished = true;
170 break;
171 }
172 }
173 }
174 if point_count == 0 {
175 return None;
176 }
177 let cloud = match PointCloud::try_from_parts(schema, buffers, self.metadata.clone()) {
178 Ok(cloud) => cloud,
179 Err(error) => return Some(Err(error.into())),
180 };
181 Some(self.state.lease(cloud, reservation))
182 }
183}
184
185fn read_points_from_reader(
186 reader: &mut Reader,
187 schema: PointSchema,
188 metadata: SpatialMetadata,
189) -> Result<PointCloud, IoError> {
190 let mut points = Vec::new();
191 for point in reader.points() {
192 points.push(point.map_err(|error| las_parse(error.to_string()))?);
193 }
194 point_cloud_from_las_points(schema, metadata, points)
195}
196
197pub(crate) fn point_cloud_from_las_points(
199 schema: PointSchema,
200 metadata: SpatialMetadata,
201 points: impl IntoIterator<Item = Point>,
202) -> Result<PointCloud, IoError> {
203 let points = points.into_iter();
204 let capacity = points.size_hint().0;
205 let mut buffers = PointBufferSet::new();
206 for field in schema.fields() {
207 buffers.insert(field.name.clone(), PointBuffer::with_capacity(field.dtype, capacity));
208 }
209
210 for point in points {
211 append_las_point(&schema, &mut buffers, &point)?;
212 }
213
214 PointCloud::try_from_parts(schema, buffers, metadata).map_err(IoError::from)
215}
216
217fn metadata_from_header(_header: &Header) -> SpatialMetadata {
218 metadata_from_las_header()
219}
220
221pub(crate) fn metadata_from_las_header() -> SpatialMetadata {
222 SpatialMetadata {
223 frame_id: spatialrust_core::FrameId::new("las"),
224 timestamp: spatialrust_core::Timestamp::from_nanos(0),
225 sensor_origin: None,
226 unit: "meter".to_owned(),
227 }
228}
229
230fn append_las_point(
231 schema: &PointSchema,
232 buffers: &mut PointBufferSet,
233 point: &Point,
234) -> Result<(), IoError> {
235 for field in schema.fields() {
236 let value = read_las_field(point, field)?;
237 push_field(buffers, field, value)?;
238 }
239 Ok(())
240}
241
242fn read_las_field(point: &Point, field: &spatialrust_core::PointField) -> Result<f64, IoError> {
243 match field.semantic {
244 FieldSemantic::PositionX => Ok(point.x),
245 FieldSemantic::PositionY => Ok(point.y),
246 FieldSemantic::PositionZ => Ok(point.z),
247 FieldSemantic::Intensity => Ok(f64::from(point.intensity)),
248 FieldSemantic::Label => Ok(f64::from(u8::from(point.classification))),
249 FieldSemantic::TimeOffset => point
250 .gps_time
251 .ok_or_else(|| las_parse("missing gps_time for LAS point format".to_owned())),
252 FieldSemantic::ColorR => point
253 .color
254 .map(|color| f64::from(color.red))
255 .ok_or_else(|| las_parse("missing color for LAS point format".to_owned())),
256 FieldSemantic::ColorG => point
257 .color
258 .map(|color| f64::from(color.green))
259 .ok_or_else(|| las_parse("missing color for LAS point format".to_owned())),
260 FieldSemantic::ColorB => point
261 .color
262 .map(|color| f64::from(color.blue))
263 .ok_or_else(|| las_parse("missing color for LAS point format".to_owned())),
264 _ => Err(las_parse(format!("unsupported LAS field `{}`", field.name))),
265 }
266}
267
268fn push_field(
269 buffers: &mut PointBufferSet,
270 field: &spatialrust_core::PointField,
271 value: f64,
272) -> Result<(), IoError> {
273 let buffer = buffers
274 .get_mut(&field.name)
275 .ok_or_else(|| spatialrust_core::SpatialError::MissingField(field.name.clone()))?;
276 match field.dtype {
277 DType::F32 | DType::F16 => {
278 buffer.push_f32(value as f32).map_err(IoError::from)?;
279 Ok(())
280 }
281 DType::F64 => {
282 buffer.push_f64(value).map_err(IoError::from)?;
283 Ok(())
284 }
285 DType::U8 => {
286 buffer.push_u8(value.round() as u8).map_err(IoError::from)?;
287 Ok(())
288 }
289 DType::U16 => {
290 buffer.push_u16(value.round() as u16).map_err(IoError::from)?;
291 Ok(())
292 }
293 DType::I32 => {
294 buffer.push_i32(value.round() as i32).map_err(IoError::from)?;
295 Ok(())
296 }
297 DType::U32 => {
298 let PointBuffer::U32(values) = buffer else {
299 return Err(spatialrust_core::SpatialError::UnsupportedDType(field.dtype).into());
300 };
301 values.push(value.round() as u32);
302 Ok(())
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::read_las;
310 use crate::las::writer::{write_las, LasWriteFormat};
311 use spatialrust_core::{HasIntensity, HasPositions3, PointCloudBuilder, StandardSchemas};
312 use std::io::Cursor;
313
314 #[test]
315 fn roundtrip_xyz_intensity() {
316 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyzi());
317 builder.push_point([1.0, 2.0, 3.0, 100.0]).unwrap();
318 builder.push_point([4.0, 5.0, 6.0, 200.0]).unwrap();
319 let cloud = builder.build().unwrap();
320
321 let mut cursor = write_las(Cursor::new(Vec::new()), &cloud, LasWriteFormat::Las).unwrap();
322 cursor.set_position(0);
323 let loaded = read_las(cursor).unwrap();
324
325 assert_eq!(loaded.len(), 2);
326 let (x, y, z) = loaded.positions3().unwrap();
327 assert!((x[0] - 1.0).abs() < 1e-5);
328 assert!((y[1] - 5.0).abs() < 1e-5);
329 assert!((z[1] - 6.0).abs() < 1e-5);
330 assert!((loaded.intensity().unwrap()[0] - 100.0).abs() < 1e-3);
331 }
332}