1#[allow(dead_code)]
4pub struct LasHeader {
5 pub version_major: u8,
6 pub version_minor: u8,
7 pub point_count: u32,
8 pub x_scale: f64,
9 pub y_scale: f64,
10 pub z_scale: f64,
11 pub x_offset: f64,
12 pub y_offset: f64,
13 pub z_offset: f64,
14}
15
16#[allow(dead_code)]
17pub struct LasPoint {
18 pub x: i32, pub y: i32,
20 pub z: i32,
21 pub intensity: u16,
22 pub classification: u8,
23}
24
25#[allow(dead_code)]
26pub struct LasFile {
27 pub header: LasHeader,
28 pub points: Vec<LasPoint>,
29}
30
31#[allow(dead_code)]
32pub struct E57Stub {
33 pub point_count: usize,
34 pub has_color: bool,
35 pub xml_header: String,
36}
37
38#[allow(dead_code)]
39pub fn new_las_header(point_count: u32, scale: f64) -> LasHeader {
40 LasHeader {
41 version_major: 1,
42 version_minor: 4,
43 point_count,
44 x_scale: scale,
45 y_scale: scale,
46 z_scale: scale,
47 x_offset: 0.0,
48 y_offset: 0.0,
49 z_offset: 0.0,
50 }
51}
52
53#[allow(dead_code)]
54pub fn positions_to_las(positions: &[[f32; 3]], scale: f64) -> LasFile {
55 let header = new_las_header(positions.len() as u32, scale);
56 let points: Vec<LasPoint> = positions
57 .iter()
58 .map(|p| LasPoint {
59 x: (p[0] as f64 / scale).round() as i32,
60 y: (p[1] as f64 / scale).round() as i32,
61 z: (p[2] as f64 / scale).round() as i32,
62 intensity: 0,
63 classification: 0,
64 })
65 .collect();
66 LasFile { header, points }
67}
68
69#[allow(dead_code)]
70pub fn las_point_to_world(point: &LasPoint, header: &LasHeader) -> [f64; 3] {
71 [
72 point.x as f64 * header.x_scale + header.x_offset,
73 point.y as f64 * header.y_scale + header.y_offset,
74 point.z as f64 * header.z_scale + header.z_offset,
75 ]
76}
77
78#[allow(dead_code)]
79pub fn las_file_size_estimate(las: &LasFile) -> usize {
80 375 + las.points.len() * 20
82}
83
84#[allow(dead_code)]
85pub fn export_las_binary_stub(las: &LasFile) -> Vec<u8> {
86 let mut bytes = Vec::new();
88 bytes.extend_from_slice(b"LASF");
90 bytes.push(las.header.version_major);
92 bytes.push(las.header.version_minor);
93 bytes.extend_from_slice(&las.header.point_count.to_le_bytes());
95 bytes.extend_from_slice(&las.header.x_scale.to_le_bytes());
97 bytes.extend_from_slice(&las.header.y_scale.to_le_bytes());
98 bytes.extend_from_slice(&las.header.z_scale.to_le_bytes());
99 for p in &las.points {
101 bytes.extend_from_slice(&p.x.to_le_bytes());
102 bytes.extend_from_slice(&p.y.to_le_bytes());
103 bytes.extend_from_slice(&p.z.to_le_bytes());
104 bytes.extend_from_slice(&p.intensity.to_le_bytes());
105 bytes.push(p.classification);
106 }
107 bytes
108}
109
110#[allow(dead_code)]
111pub fn new_e57_stub(point_count: usize, has_color: bool) -> E57Stub {
112 let xml_header = format!(
113 "<?xml version=\"1.0\"?><e57Root pointCount=\"{}\" hasColor=\"{}\"/>",
114 point_count, has_color
115 );
116 E57Stub {
117 point_count,
118 has_color,
119 xml_header,
120 }
121}
122
123#[allow(dead_code)]
124pub fn e57_xml_header(stub: &E57Stub) -> String {
125 stub.xml_header.clone()
126}
127
128#[allow(dead_code)]
130pub fn las_bounds(las: &LasFile) -> ([i32; 3], [i32; 3]) {
131 if las.points.is_empty() {
132 return ([0, 0, 0], [0, 0, 0]);
133 }
134 let mut min = [i32::MAX; 3];
135 let mut max = [i32::MIN; 3];
136 for p in &las.points {
137 let coords = [p.x, p.y, p.z];
138 for k in 0..3 {
139 if coords[k] < min[k] {
140 min[k] = coords[k];
141 }
142 if coords[k] > max[k] {
143 max[k] = coords[k];
144 }
145 }
146 }
147 (min, max)
148}
149
150#[allow(dead_code)]
151pub fn las_point_count(las: &LasFile) -> usize {
152 las.points.len()
153}
154
155#[allow(dead_code)]
156pub fn filter_las_by_classification(las: &LasFile, class: u8) -> Vec<&LasPoint> {
157 las.points
158 .iter()
159 .filter(|p| p.classification == class)
160 .collect()
161}
162
163#[allow(dead_code)]
164pub fn las_to_positions(las: &LasFile) -> Vec<[f32; 3]> {
165 las.points
166 .iter()
167 .map(|p| {
168 let w = las_point_to_world(p, &las.header);
169 [w[0] as f32, w[1] as f32, w[2] as f32]
170 })
171 .collect()
172}
173
174#[allow(dead_code)]
175pub fn decimate_las(las: &LasFile, keep_every: usize) -> LasFile {
176 let keep = keep_every.max(1);
177 let points: Vec<LasPoint> = las
178 .points
179 .iter()
180 .enumerate()
181 .filter_map(|(i, p)| {
182 if i % keep == 0 {
183 Some(LasPoint {
184 x: p.x,
185 y: p.y,
186 z: p.z,
187 intensity: p.intensity,
188 classification: p.classification,
189 })
190 } else {
191 None
192 }
193 })
194 .collect();
195 let new_count = points.len() as u32;
196 let header = LasHeader {
197 version_major: las.header.version_major,
198 version_minor: las.header.version_minor,
199 point_count: new_count,
200 x_scale: las.header.x_scale,
201 y_scale: las.header.y_scale,
202 z_scale: las.header.z_scale,
203 x_offset: las.header.x_offset,
204 y_offset: las.header.y_offset,
205 z_offset: las.header.z_offset,
206 };
207 LasFile { header, points }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn sample_positions() -> Vec<[f32; 3]> {
215 vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]
216 }
217
218 #[test]
219 fn test_new_las_header() {
220 let h = new_las_header(100, 0.001);
221 assert_eq!(h.version_major, 1);
222 assert_eq!(h.version_minor, 4);
223 assert_eq!(h.point_count, 100);
224 assert!((h.x_scale - 0.001).abs() < 1e-9);
225 }
226
227 #[test]
228 fn test_positions_to_las() {
229 let positions = sample_positions();
230 let las = positions_to_las(&positions, 0.001);
231 assert_eq!(las.points.len(), 3);
232 assert_eq!(las.header.point_count, 3);
233 }
234
235 #[test]
236 fn test_las_point_to_world_round_trip() {
237 let positions = vec![[1.5_f32, 2.5, 3.5]];
238 let scale = 0.001;
239 let las = positions_to_las(&positions, scale);
240 let world = las_point_to_world(&las.points[0], &las.header);
241 assert!((world[0] - 1.5).abs() < 0.01);
242 assert!((world[1] - 2.5).abs() < 0.01);
243 assert!((world[2] - 3.5).abs() < 0.01);
244 }
245
246 #[test]
247 fn test_las_point_count() {
248 let las = positions_to_las(&sample_positions(), 0.001);
249 assert_eq!(las_point_count(&las), 3);
250 }
251
252 #[test]
253 fn test_las_bounds() {
254 let las = positions_to_las(&sample_positions(), 1.0);
255 let (mn, mx) = las_bounds(&las);
256 assert!(mn[0] <= mx[0]);
257 assert!(mn[1] <= mx[1]);
258 assert!(mn[2] <= mx[2]);
259 }
260
261 #[test]
262 fn test_las_bounds_empty() {
263 let header = new_las_header(0, 0.001);
264 let las = LasFile {
265 header,
266 points: vec![],
267 };
268 let (mn, mx) = las_bounds(&las);
269 assert_eq!(mn, [0, 0, 0]);
270 assert_eq!(mx, [0, 0, 0]);
271 }
272
273 #[test]
274 fn test_filter_by_classification() {
275 let mut las = positions_to_las(&sample_positions(), 0.001);
276 las.points[0].classification = 1;
277 las.points[1].classification = 2;
278 las.points[2].classification = 1;
279 let filtered = filter_las_by_classification(&las, 1);
280 assert_eq!(filtered.len(), 2);
281 }
282
283 #[test]
284 fn test_las_to_positions() {
285 let positions = sample_positions();
286 let las = positions_to_las(&positions, 0.001);
287 let back = las_to_positions(&las);
288 assert_eq!(back.len(), 3);
289 assert!((back[0][0] - 1.0).abs() < 0.01);
290 }
291
292 #[test]
293 fn test_decimate_las() {
294 let positions: Vec<[f32; 3]> = (0..10).map(|i| [i as f32, 0.0, 0.0]).collect();
295 let las = positions_to_las(&positions, 0.001);
296 let dec = decimate_las(&las, 2);
297 assert_eq!(dec.points.len(), 5);
298 }
299
300 #[test]
301 fn test_decimate_las_keep_all() {
302 let positions = sample_positions();
303 let las = positions_to_las(&positions, 0.001);
304 let dec = decimate_las(&las, 1);
305 assert_eq!(dec.points.len(), 3);
306 }
307
308 #[test]
309 fn test_e57_stub() {
310 let stub = new_e57_stub(500, true);
311 assert_eq!(stub.point_count, 500);
312 assert!(stub.has_color);
313 let xml = e57_xml_header(&stub);
314 assert!(xml.contains("500"));
315 }
316
317 #[test]
318 fn test_binary_stub_non_empty() {
319 let las = positions_to_las(&sample_positions(), 0.001);
320 let bytes = export_las_binary_stub(&las);
321 assert!(!bytes.is_empty());
322 assert!(bytes.starts_with(b"LASF"));
323 }
324
325 #[test]
326 fn test_las_file_size_estimate() {
327 let las = positions_to_las(&sample_positions(), 0.001);
328 let size = las_file_size_estimate(&las);
329 assert!(size > 375);
330 }
331
332 #[test]
333 fn test_positions_to_las_empty() {
334 let las = positions_to_las(&[], 0.001);
335 assert_eq!(las.points.len(), 0);
336 assert_eq!(las.header.point_count, 0);
337 }
338}