Skip to main content

spatialrust_io/las/
reader.rs

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
21/// Reads point clouds from LAS/LAZ files.
22pub struct LasReader {
23    reader: Reader,
24    metadata: SpatialMetadata,
25    schema: PointSchema,
26    loaded: bool,
27}
28
29impl LasReader {
30    /// Opens a LAS/LAZ file and parses its header eagerly.
31    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    /// Returns the parsed LAS header.
43    #[must_use]
44    pub fn header(&self) -> &Header {
45        self.reader.header()
46    }
47
48    /// Reads the point cloud payload.
49    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
72/// Reads a complete LAS/LAZ stream.
73pub 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
83/// Reads a LAS/LAZ file from disk.
84pub 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/// Bounded sequential LAS/LAZ source.
90#[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    /// Opens a LAS or feature-enabled LAZ file for bounded chunk reads.
101    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    /// Returns the parsed LAS/LAZ header.
114    #[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
197/// Builds a point cloud from LAS points using a precomputed schema and metadata.
198pub(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}