1use crate::Error::{InvalidFileExtension, NoFileName};
2use crate::FILE_EXTENSION_XYZ_FORMAT;
3use crate::error::Error;
4use crate::xyz::{DEFAULT_XYZ_SEPARATOR, FILE_EXTENSION_XYZ_ZST_FORMAT};
5use ecoord::FrameId;
6use epoint_core::PointDataColumnType;
7use epoint_core::point_cloud::PointCloud;
8use palette::Srgb;
9use polars::prelude::{CsvWriter, NamedFrom, SerWriter, Series};
10use rayon::iter::ParallelIterator;
11use rayon::prelude::IntoParallelIterator;
12use std::fs::{File, OpenOptions};
13use std::io::{BufWriter, Write};
14use std::path::Path;
15
16pub const DEFAULT_COMPRESSION_LEVEL: i32 = 10;
17pub const DEFAULT_NULL_VALUE: &str = "NaN";
18
19#[derive(Debug, Clone)]
22pub struct XyzWriter<W: Write> {
23 writer: W,
24 compression_level: Option<i32>,
25 frame_id: Option<FrameId>,
26 separator: u8,
27 null_value: String,
28 color_depth: ColorDepth,
29}
30
31#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
32pub enum ColorDepth {
33 #[default]
34 EightBit,
35 SixteenBit,
36}
37
38impl<W: Write> XyzWriter<W> {
39 pub fn new(writer: W) -> Self {
40 Self {
41 writer,
42 compression_level: Some(crate::epoint::write::DEFAULT_COMPRESSION_LEVEL),
43 frame_id: None,
44 separator: DEFAULT_XYZ_SEPARATOR,
45 null_value: DEFAULT_NULL_VALUE.to_string(),
46 color_depth: ColorDepth::default(),
47 }
48 }
49
50 pub fn with_compressed(mut self, compressed: bool) -> Self {
51 if compressed {
52 self.compression_level = Some(DEFAULT_COMPRESSION_LEVEL);
53 } else {
54 self.compression_level = None;
55 }
56 self
57 }
58
59 pub fn with_frame_id(mut self, frame_id: FrameId) -> Self {
60 self.frame_id = Some(frame_id);
61 self
62 }
63
64 pub fn with_separator(mut self, separator: u8) -> Self {
65 self.separator = separator;
66 self
67 }
68
69 pub fn with_null_value(mut self, null_value: String) -> Self {
70 self.null_value = null_value;
71 self
72 }
73
74 pub fn with_color_depth(mut self, color_depth: ColorDepth) -> Self {
75 self.color_depth = color_depth;
76 self
77 }
78
79 pub fn finish(self, mut point_cloud: PointCloud) -> Result<(), Error> {
80 if let Some(frame_id) = &self.frame_id {
81 point_cloud.resolve_to_frame(frame_id.clone())?;
82 }
83 if point_cloud.contains_colors() {
92 match self.color_depth {
93 ColorDepth::EightBit => {
94 let converted_colors: Vec<Srgb<u8>> = point_cloud
95 .point_data
96 .get_all_colors()?
97 .into_par_iter()
98 .map(|x| x.into_format())
99 .collect();
100
101 let color_red_series = Series::new(
102 PointDataColumnType::X.into(),
103 converted_colors.iter().map(|c| c.red).collect::<Vec<u8>>(),
104 );
105 point_cloud
106 .point_data
107 .data_frame
108 .replace(PointDataColumnType::ColorRed.as_str(), color_red_series)?;
109
110 let color_green_series = Series::new(
111 PointDataColumnType::Y.into(),
112 converted_colors
113 .iter()
114 .map(|c| c.green)
115 .collect::<Vec<u8>>(),
116 );
117 point_cloud
118 .point_data
119 .data_frame
120 .replace(PointDataColumnType::ColorGreen.as_str(), color_green_series)?;
121
122 let color_blue_series = Series::new(
123 PointDataColumnType::Z.into(),
124 converted_colors.iter().map(|c| c.blue).collect::<Vec<u8>>(),
125 );
126 point_cloud
127 .point_data
128 .data_frame
129 .replace(PointDataColumnType::ColorBlue.as_str(), color_blue_series)?;
130 }
131 ColorDepth::SixteenBit => {}
132 }
133 }
134
135 let writer: Box<dyn Write> = if let Some(compression_level) = &self.compression_level {
136 let buf_writer = BufWriter::with_capacity(
137 zstd::stream::Encoder::<Vec<u8>>::recommended_input_size(),
138 zstd::stream::Encoder::new(self.writer, *compression_level)?.auto_finish(),
139 );
140 Box::new(buf_writer)
141 } else {
142 Box::new(self.writer)
143 };
144
145 CsvWriter::new(writer)
146 .with_separator(self.separator)
147 .with_null_value(self.null_value)
148 .finish(&mut point_cloud.point_data.data_frame)?;
149
150 Ok(())
151 }
152}
153
154impl XyzWriter<File> {
155 pub fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
156 let file_name_str = path
157 .as_ref()
158 .file_name()
159 .ok_or(NoFileName())?
160 .to_string_lossy()
161 .to_lowercase();
162 if !file_name_str.ends_with(FILE_EXTENSION_XYZ_ZST_FORMAT)
163 && !file_name_str.ends_with(FILE_EXTENSION_XYZ_FORMAT)
164 {
165 return Err(InvalidFileExtension(file_name_str.to_string()));
166 }
167
168 let file = OpenOptions::new()
169 .create(true)
170 .write(true)
171 .truncate(true)
172 .open(path)?;
173 Ok(Self::new(file))
174 }
175}