1use std::io::{Seek, Write};
2
3use las::point::{Classification, Format};
4use las::{Builder, Color, Header, Point, Writer};
5use spatialrust_core::{FieldSemantic, HasPositions3, PointCloud, PointField, PointSchema};
6
7use crate::error::{las_format, las_parse, IoError};
8use crate::las::schema::schema_from_point_cloud;
9use crate::{PointWriter, WriteOptions};
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum LasWriteFormat {
14 #[default]
16 Las,
17 Laz,
19}
20
21pub struct LasWriter<W: Write + Seek + Send + Sync + 'static> {
23 writer: W,
24 format: LasWriteFormat,
25}
26
27impl<W: Write + Seek + Send + Sync + 'static> LasWriter<W> {
28 #[must_use]
30 pub const fn new(writer: W, format: LasWriteFormat) -> Self {
31 Self { writer, format }
32 }
33}
34
35impl<W: Write + Seek + Send + Sync + 'static> PointWriter for LasWriter<W> {
36 fn write(
37 &mut self,
38 cloud: &PointCloud,
39 _options: &WriteOptions,
40 ) -> spatialrust_core::SpatialResult<()> {
41 let buffer = write_las(std::io::Cursor::new(Vec::new()), cloud, self.format)
42 .map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))?;
43 self.writer
44 .write_all(buffer.get_ref())
45 .map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))
46 }
47}
48
49pub fn write_las<W: Write + Seek + Send + Sync + 'static>(
51 writer: W,
52 cloud: &PointCloud,
53 format: LasWriteFormat,
54) -> Result<W, IoError> {
55 cloud.validate()?;
56 if format == LasWriteFormat::Laz {
57 #[cfg(not(feature = "io-laz"))]
58 {
59 return Err(crate::error::laz_format(
60 "LAZ output requires the io-laz feature".to_owned(),
61 ));
62 }
63 }
64
65 let (point_format, export_schema) = schema_from_point_cloud(cloud.schema())?;
66 let header = header_from_cloud(point_format, format)?;
67 let mut las_writer =
68 Writer::new(writer, header).map_err(|error| las_format(error.to_string()))?;
69
70 for index in 0..cloud.len() {
71 let point = point_from_cloud(cloud, &export_schema, index, point_format)?;
72 las_writer.write_point(point).map_err(|error| las_format(error.to_string()))?;
73 }
74
75 las_writer.into_inner().map_err(|error| las_format(error.to_string()))
76}
77
78pub fn write_las_file(
80 path: impl AsRef<std::path::Path>,
81 cloud: &PointCloud,
82 format: LasWriteFormat,
83) -> Result<(), IoError> {
84 if format == LasWriteFormat::Laz {
85 #[cfg(not(feature = "io-laz"))]
86 {
87 return Err(crate::error::laz_format(
88 "LAZ output requires the io-laz feature".to_owned(),
89 ));
90 }
91 }
92
93 let (point_format, export_schema) = schema_from_point_cloud(cloud.schema())?;
94 let header = header_from_cloud(point_format, format)?;
95 let mut las_writer =
96 Writer::from_path(path.as_ref(), header).map_err(|error| las_format(error.to_string()))?;
97
98 for index in 0..cloud.len() {
99 let point = point_from_cloud(cloud, &export_schema, index, point_format)?;
100 las_writer.write_point(point).map_err(|error| las_format(error.to_string()))?;
101 }
102
103 las_writer.close().map_err(|error| las_format(error.to_string()))
104}
105
106fn header_from_cloud(point_format: Format, format: LasWriteFormat) -> Result<Header, IoError> {
107 let mut builder = Builder::from((1, 2));
108 builder.point_format = point_format;
109 builder.system_identifier = "SpatialRust".to_owned();
110 builder.generating_software = "SpatialRust".to_owned();
111
112 if format == LasWriteFormat::Laz {
113 builder.point_format.is_compressed = true;
114 }
115
116 builder.into_header().map_err(|error| las_format(error.to_string()))
117}
118
119pub(crate) fn point_from_cloud(
120 cloud: &PointCloud,
121 schema: &PointSchema,
122 index: usize,
123 format: Format,
124) -> Result<Point, IoError> {
125 let (x, y, z) = cloud.positions3()?;
126 let mut point = Point {
127 x: f64::from(x[index]),
128 y: f64::from(y[index]),
129 z: f64::from(z[index]),
130 ..Default::default()
131 };
132
133 for field in schema.fields() {
134 if matches!(
135 field.semantic,
136 FieldSemantic::PositionX | FieldSemantic::PositionY | FieldSemantic::PositionZ
137 ) {
138 continue;
139 }
140 let Some(field_name) = cloud_field_name_for_export(cloud, field) else {
141 continue;
142 };
143 let value = read_cloud_field(cloud, field_name, index)?;
144 apply_las_field(&mut point, field, value, format)?;
145 }
146
147 Ok(point)
148}
149
150fn cloud_field_name_for_export<'a>(
151 cloud: &'a PointCloud,
152 export_field: &'a PointField,
153) -> Option<&'a str> {
154 if cloud.field(&export_field.name).is_ok() {
155 return Some(export_field.name.as_str());
156 }
157 cloud
158 .schema()
159 .find_semantic(export_field.semantic)
160 .map(|field| field.name.as_str())
161 .filter(|name| cloud.field(name).is_ok())
162}
163
164fn read_cloud_field(cloud: &PointCloud, field_name: &str, index: usize) -> Result<f64, IoError> {
165 let buffer = cloud.field(field_name)?;
166 Ok(match buffer {
167 PointBuffer::F32(values) => f64::from(values[index]),
168 PointBuffer::F64(values) => values[index],
169 PointBuffer::U8(values) => f64::from(values[index]),
170 PointBuffer::U16(values) => f64::from(values[index]),
171 PointBuffer::I32(values) => f64::from(values[index]),
172 PointBuffer::U32(values) => f64::from(values[index]),
173 })
174}
175
176use spatialrust_core::PointBuffer;
177
178fn apply_las_field(
179 point: &mut Point,
180 field: &PointField,
181 value: f64,
182 format: Format,
183) -> Result<(), IoError> {
184 match field.semantic {
185 FieldSemantic::Intensity => point.intensity = value.round() as u16,
186 FieldSemantic::Label => {
187 point.classification = Classification::new(value.round() as u8)
188 .map_err(|error| las_parse(error.to_string()))?;
189 }
190 FieldSemantic::TimeOffset => {
191 if format.has_gps_time {
192 point.gps_time = Some(value);
193 }
194 }
195 FieldSemantic::ColorR | FieldSemantic::ColorG | FieldSemantic::ColorB => {
196 if format.has_color {
197 let color = point.color.get_or_insert_with(Color::default);
198 match field.semantic {
199 FieldSemantic::ColorR => color.red = value.round() as u16,
200 FieldSemantic::ColorG => color.green = value.round() as u16,
201 FieldSemantic::ColorB => color.blue = value.round() as u16,
202 _ => {}
203 }
204 }
205 }
206 FieldSemantic::PositionX | FieldSemantic::PositionY | FieldSemantic::PositionZ => {}
207 _ => {}
208 }
209 Ok(())
210}
211
212#[cfg(test)]
213mod tests {
214 use super::{write_las, LasWriteFormat};
215 use spatialrust_core::PointCloudBuilder;
216 use std::io::Cursor;
217
218 #[test]
219 fn writes_xyz_cloud() {
220 let mut builder = PointCloudBuilder::xyz();
221 builder.push_point([1.0, 2.0, 3.0]).unwrap();
222 let cloud = builder.build().unwrap();
223 let bytes = write_las(Cursor::new(Vec::new()), &cloud, LasWriteFormat::Las).unwrap();
224 assert!(!bytes.get_ref().is_empty());
225 }
226
227 #[test]
228 fn writes_xyzirgb_cloud_with_u8_color_fields() {
229 use spatialrust_core::StandardSchemas;
230
231 let mut builder = PointCloudBuilder::new(StandardSchemas::point_xyzirgb());
232 builder.push_point([1.0, 2.0, 3.0, 100.0, 255.0, 128.0, 64.0]).unwrap();
233 let cloud = builder.build().unwrap();
234 let bytes = write_las(Cursor::new(Vec::new()), &cloud, LasWriteFormat::Las).unwrap();
235 assert!(!bytes.get_ref().is_empty());
236 }
237
238 #[cfg(feature = "io-laz")]
239 #[test]
240 fn writes_laz_file() {
241 use super::write_las_file;
242
243 let mut builder = PointCloudBuilder::xyz();
244 builder.push_point([0.0, 0.0, 0.0]).unwrap();
245 let cloud = builder.build().unwrap();
246 let path =
247 std::env::temp_dir().join(format!("spatialrust_laz_write_{}.laz", std::process::id()));
248 write_las_file(&path, &cloud, LasWriteFormat::Laz).unwrap();
249 let loaded = crate::las::read_las_file(&path).unwrap();
250 let _ = std::fs::remove_file(path);
251 assert_eq!(loaded.len(), 1);
252 }
253}