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
13pub struct LasReader {
15 reader: Reader,
16 metadata: SpatialMetadata,
17 schema: PointSchema,
18 loaded: bool,
19}
20
21impl LasReader {
22 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 #[must_use]
36 pub fn header(&self) -> &Header {
37 self.reader.header()
38 }
39
40 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
64pub 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
75pub 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
93pub(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}