Skip to main content

spatialrust_io/e57/
writer.rs

1use std::path::Path;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use e57::{E57Writer as ExternalE57Writer, RawValues, RecordValue};
5use spatialrust_core::{
6    DType, FieldSemantic, HasPositions3, PointBuffer, PointCloud, PointField, PointSchema,
7};
8
9use crate::e57::schema::{schema_from_point_cloud, validate_export_schema};
10use crate::error::{e57_format, e57_parse, IoError};
11use crate::{PointWriter, WriteOptions};
12
13/// Writes point clouds to E57 files.
14pub struct E57Writer;
15
16/// Creates a unique E57 GUID string.
17#[must_use]
18pub fn new_e57_guid(prefix: &str) -> String {
19    let nanos = SystemTime::now()
20        .duration_since(UNIX_EPOCH)
21        .map(|duration| duration.as_nanos())
22        .unwrap_or(0);
23    format!("{prefix}-{nanos}")
24}
25
26impl PointWriter for E57Writer {
27    fn write(
28        &mut self,
29        cloud: &PointCloud,
30        _options: &WriteOptions,
31    ) -> spatialrust_core::SpatialResult<()> {
32        let path = std::env::temp_dir().join(format!("spatialrust_e57_{}.e57", std::process::id()));
33        write_e57_file(&path, cloud)
34            .map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))?;
35        std::fs::remove_file(path)
36            .map_err(|error| spatialrust_core::SpatialError::Io(error.to_string()))?;
37        Ok(())
38    }
39}
40
41/// Writes a point cloud to an E57 file on disk.
42pub fn write_e57(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
43    write_e57_file(path, cloud)
44}
45
46/// Writes a point cloud to an E57 file on disk.
47pub fn write_e57_file(path: impl AsRef<Path>, cloud: &PointCloud) -> Result<(), IoError> {
48    cloud.validate()?;
49    validate_export_schema(cloud.schema())?;
50
51    let (export_schema, prototype) = schema_from_point_cloud(cloud.schema());
52    let file_guid = new_e57_guid("spatialrust");
53    let mut writer = ExternalE57Writer::from_file(path, &file_guid)
54        .map_err(|error| e57_format(error.to_string()))?;
55
56    let scan_guid = new_e57_guid("scan");
57    let mut pc_writer = writer
58        .add_pointcloud(&scan_guid, prototype)
59        .map_err(|error| e57_format(error.to_string()))?;
60
61    for index in 0..cloud.len() {
62        let values = point_values(cloud, &export_schema, index)?;
63        pc_writer.add_point(values).map_err(|error| e57_format(error.to_string()))?;
64    }
65
66    pc_writer.finalize().map_err(|error| e57_format(error.to_string()))?;
67    writer.finalize().map_err(|error| e57_format(error.to_string()))
68}
69
70fn point_values(
71    cloud: &PointCloud,
72    schema: &PointSchema,
73    index: usize,
74) -> Result<RawValues, IoError> {
75    let (x, y, z) = cloud.positions3()?;
76    let mut values = Vec::with_capacity(schema.len());
77
78    for field in schema.fields() {
79        let value = match field.semantic {
80            FieldSemantic::PositionX => RecordValue::Single(x[index]),
81            FieldSemantic::PositionY => RecordValue::Single(y[index]),
82            FieldSemantic::PositionZ => RecordValue::Single(z[index]),
83            FieldSemantic::Intensity => RecordValue::Single(read_cloud_field(cloud, field, index)?),
84            FieldSemantic::ColorR | FieldSemantic::ColorG | FieldSemantic::ColorB => {
85                RecordValue::Integer(read_cloud_field(cloud, field, index)?.round() as i64)
86            }
87            _ => {
88                return Err(e57_parse(format!(
89                    "unsupported E57 field `{}` during export",
90                    field.name
91                )));
92            }
93        };
94        values.push(value);
95    }
96
97    Ok(values)
98}
99
100fn read_cloud_field(cloud: &PointCloud, field: &PointField, index: usize) -> Result<f32, IoError> {
101    let buffer = cloud.field(&field.name)?;
102    match field.dtype {
103        DType::F32 | DType::F16 => Ok(buffer.as_f32()?[index]),
104        DType::U8 => {
105            let PointBuffer::U8(values) = buffer else {
106                return Err(spatialrust_core::SpatialError::UnsupportedDType(field.dtype).into());
107            };
108            Ok(f32::from(values[index]))
109        }
110        DType::U16 => {
111            let PointBuffer::U16(values) = buffer else {
112                return Err(spatialrust_core::SpatialError::UnsupportedDType(field.dtype).into());
113            };
114            Ok(f32::from(values[index]))
115        }
116        _ => Err(spatialrust_core::SpatialError::UnsupportedDType(field.dtype).into()),
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::write_e57_file;
123    use crate::e57::reader::read_e57_file;
124    use spatialrust_core::PointCloudBuilder;
125
126    #[test]
127    fn writes_xyz_cloud() {
128        let mut builder = PointCloudBuilder::xyz();
129        builder.push_point([1.0, 2.0, 3.0]).unwrap();
130        let cloud = builder.build().unwrap();
131        let path =
132            std::env::temp_dir().join(format!("spatialrust_e57_write_{}.e57", std::process::id()));
133        write_e57_file(&path, &cloud).unwrap();
134        let loaded = read_e57_file(&path).unwrap();
135        let _ = std::fs::remove_file(path);
136        assert_eq!(loaded.len(), 1);
137    }
138}