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/// Reads point clouds from LAS/LAZ files.
14pub struct LasReader {
15    reader: Reader,
16    metadata: SpatialMetadata,
17    schema: PointSchema,
18    loaded: bool,
19}
20
21impl LasReader {
22    /// Opens a LAS/LAZ file and parses its header eagerly.
23    pub fn open(path: impl AsRef<Path>) -> Result<Self, IoError> {
24        let reader = Reader::from_path(path).map_err(|error| las_parse(error.to_string()))?;
25        let header = reader.header();
26        Ok(Self {
27            schema: schema_for_las_header(header),
28            metadata: metadata_from_header(header),
29            reader,
30            loaded: false,
31        })
32    }
33
34    /// Returns the parsed LAS header.
35    #[must_use]
36    pub fn header(&self) -> &Header {
37        self.reader.header()
38    }
39
40    /// Reads the point cloud payload.
41    pub fn read_cloud(&mut self) -> Result<PointCloud, IoError> {
42        if self.loaded {
43            return Err(crate::error::las_format("LAS reader already consumed"));
44        }
45        self.loaded = true;
46        read_points_from_reader(&mut self.reader, self.schema.clone(), self.metadata.clone())
47    }
48}
49
50impl PointReader for LasReader {
51    fn schema(&self) -> spatialrust_core::SpatialResult<PointSchema> {
52        Ok(self.schema.clone())
53    }
54
55    fn metadata(&self) -> spatialrust_core::SpatialResult<SpatialMetadata> {
56        Ok(self.metadata.clone())
57    }
58
59    fn read(&mut self, _options: &ReadOptions) -> spatialrust_core::SpatialResult<PointCloud> {
60        self.read_cloud().map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))
61    }
62}
63
64/// Reads a complete LAS/LAZ stream.
65pub fn read_las<R: BufRead + Seek + Send + Sync + 'static>(
66    reader: R,
67) -> Result<PointCloud, IoError> {
68    let mut las_reader = Reader::new(reader).map_err(|error| las_parse(error.to_string()))?;
69    let header = las_reader.header().clone();
70    let schema = schema_for_las_header(&header);
71    let metadata = metadata_from_header(&header);
72    read_points_from_reader(&mut las_reader, schema, metadata)
73}
74
75/// Reads a LAS/LAZ file from disk.
76pub fn read_las_file(path: impl AsRef<Path>) -> Result<PointCloud, IoError> {
77    let mut reader = LasReader::open(path)?;
78    reader.read_cloud()
79}
80
81fn read_points_from_reader(
82    reader: &mut Reader,
83    schema: PointSchema,
84    metadata: SpatialMetadata,
85) -> Result<PointCloud, IoError> {
86    let mut points = Vec::new();
87    for point in reader.points() {
88        points.push(point.map_err(|error| las_parse(error.to_string()))?);
89    }
90    point_cloud_from_las_points(schema, metadata, points)
91}
92
93/// Builds a point cloud from LAS points using a precomputed schema and metadata.
94pub(crate) fn point_cloud_from_las_points(
95    schema: PointSchema,
96    metadata: SpatialMetadata,
97    points: impl IntoIterator<Item = Point>,
98) -> Result<PointCloud, IoError> {
99    let mut buffers = PointBufferSet::new();
100    for field in schema.fields() {
101        buffers.insert(field.name.clone(), PointBuffer::with_capacity(field.dtype, 0));
102    }
103
104    for point in points {
105        append_las_point(&schema, &mut buffers, &point)?;
106    }
107
108    PointCloud::try_from_parts(schema, buffers, metadata).map_err(IoError::from)
109}
110
111fn metadata_from_header(_header: &Header) -> SpatialMetadata {
112    metadata_from_las_header()
113}
114
115pub(crate) fn metadata_from_las_header() -> SpatialMetadata {
116    SpatialMetadata {
117        frame_id: spatialrust_core::FrameId::new("las"),
118        timestamp: spatialrust_core::Timestamp::from_nanos(0),
119        sensor_origin: None,
120        unit: "meter".to_owned(),
121    }
122}
123
124fn append_las_point(
125    schema: &PointSchema,
126    buffers: &mut PointBufferSet,
127    point: &Point,
128) -> Result<(), IoError> {
129    for field in schema.fields() {
130        let value = read_las_field(point, field)?;
131        push_field(buffers, field, value)?;
132    }
133    Ok(())
134}
135
136fn read_las_field(point: &Point, field: &spatialrust_core::PointField) -> Result<f64, IoError> {
137    match field.semantic {
138        FieldSemantic::PositionX => Ok(point.x),
139        FieldSemantic::PositionY => Ok(point.y),
140        FieldSemantic::PositionZ => Ok(point.z),
141        FieldSemantic::Intensity => Ok(f64::from(point.intensity)),
142        FieldSemantic::Label => Ok(f64::from(u8::from(point.classification))),
143        FieldSemantic::TimeOffset => point
144            .gps_time
145            .ok_or_else(|| las_parse("missing gps_time for LAS point format".to_owned())),
146        FieldSemantic::ColorR => point
147            .color
148            .map(|color| f64::from(color.red))
149            .ok_or_else(|| las_parse("missing color for LAS point format".to_owned())),
150        FieldSemantic::ColorG => point
151            .color
152            .map(|color| f64::from(color.green))
153            .ok_or_else(|| las_parse("missing color for LAS point format".to_owned())),
154        FieldSemantic::ColorB => point
155            .color
156            .map(|color| f64::from(color.blue))
157            .ok_or_else(|| las_parse("missing color for LAS point format".to_owned())),
158        _ => Err(las_parse(format!("unsupported LAS field `{}`", field.name))),
159    }
160}
161
162fn push_field(
163    buffers: &mut PointBufferSet,
164    field: &spatialrust_core::PointField,
165    value: f64,
166) -> Result<(), IoError> {
167    let buffer = buffers
168        .get_mut(&field.name)
169        .ok_or_else(|| spatialrust_core::SpatialError::MissingField(field.name.clone()))?;
170    match field.dtype {
171        DType::F32 | DType::F16 => {
172            buffer.push_f32(value as f32).map_err(IoError::from)?;
173            Ok(())
174        }
175        DType::F64 => {
176            buffer.push_f64(value).map_err(IoError::from)?;
177            Ok(())
178        }
179        DType::U8 => {
180            buffer.push_u8(value.round() as u8).map_err(IoError::from)?;
181            Ok(())
182        }
183        DType::U16 => {
184            buffer.push_u16(value.round() as u16).map_err(IoError::from)?;
185            Ok(())
186        }
187        DType::I32 => {
188            buffer.push_i32(value.round() as i32).map_err(IoError::from)?;
189            Ok(())
190        }
191        DType::U32 => {
192            let PointBuffer::U32(values) = buffer else {
193                return Err(spatialrust_core::SpatialError::UnsupportedDType(field.dtype).into());
194            };
195            values.push(value.round() as u32);
196            Ok(())
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::read_las;
204    use crate::las::writer::{write_las, LasWriteFormat};
205    use spatialrust_core::{HasIntensity, HasPositions3, PointCloudBuilder, StandardSchemas};
206    use std::io::Cursor;
207
208    #[test]
209    fn roundtrip_xyz_intensity() {
210        let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyzi());
211        builder.push_point([1.0, 2.0, 3.0, 100.0]).unwrap();
212        builder.push_point([4.0, 5.0, 6.0, 200.0]).unwrap();
213        let cloud = builder.build().unwrap();
214
215        let mut cursor = write_las(Cursor::new(Vec::new()), &cloud, LasWriteFormat::Las).unwrap();
216        cursor.set_position(0);
217        let loaded = read_las(cursor).unwrap();
218
219        assert_eq!(loaded.len(), 2);
220        let (x, y, z) = loaded.positions3().unwrap();
221        assert!((x[0] - 1.0).abs() < 1e-5);
222        assert!((y[1] - 5.0).abs() < 1e-5);
223        assert!((z[1] - 6.0).abs() < 1e-5);
224        assert!((loaded.intensity().unwrap()[0] - 100.0).abs() < 1e-3);
225    }
226}