1use std::fs::File;
2use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
3use std::path::Path;
4
5use byteorder::{LittleEndian, WriteBytesExt};
6use copc_core::{
7 Bounds, CancelCheck, CopcInfo, Entry, Error, LasPointRecord, NeverCancel, Result,
8 StreamingLayout, VoxelKey,
9};
10use las::Read as _;
11use laz::{LasZipCompressor, LazVlrBuilder};
12
13use crate::spill::{SpillReader, SpillWriter};
14
15const CANCEL_POLL_STRIDE: usize = 4_096;
16
17#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct CopcPointFields {
20 pub x: f64,
21 pub y: f64,
22 pub z: f64,
23 pub intensity: u16,
24 pub return_number: u8,
25 pub number_of_returns: u8,
26 pub synthetic: u8,
27 pub key_point: u8,
28 pub withheld: u8,
29 pub overlap: u8,
30 pub scan_channel: u8,
31 pub scan_direction_flag: u8,
32 pub edge_of_flight_line: u8,
33 pub classification: u8,
34 pub user_data: u8,
35 pub scan_angle_rank: i16,
36 pub point_source_id: u16,
37 pub gps_time: f64,
38 pub red: u16,
39 pub green: u16,
40 pub blue: u16,
41}
42
43pub trait CopcPointSource {
45 fn len(&self) -> usize;
46 fn xyz(&self, index: usize) -> (f64, f64, f64);
47 fn fields(&self, index: usize) -> Result<CopcPointFields>;
48
49 fn is_empty(&self) -> bool {
50 self.len() == 0
51 }
52}
53
54struct SpillSource<'a> {
55 reader: &'a SpillReader,
56}
57
58impl CopcPointSource for SpillSource<'_> {
59 fn len(&self) -> usize {
60 self.reader.len()
61 }
62
63 #[inline]
64 fn xyz(&self, index: usize) -> (f64, f64, f64) {
65 self.reader.xyz_at(index)
66 }
67
68 fn fields(&self, index: usize) -> Result<CopcPointFields> {
69 let record = self.reader.record_at(index)?;
70 Ok(CopcPointFields {
71 x: record.x,
72 y: record.y,
73 z: record.z,
74 intensity: record.intensity,
75 return_number: record.return_number,
76 number_of_returns: record.number_of_returns,
77 synthetic: u8::from(record.synthetic),
78 key_point: u8::from(record.key_point),
79 withheld: u8::from(record.withheld),
80 overlap: u8::from(record.overlap),
81 scan_channel: record.scan_channel,
82 scan_direction_flag: u8::from(record.scan_direction_flag),
83 edge_of_flight_line: u8::from(record.edge_of_flight_line),
84 classification: record.classification,
85 user_data: record.user_data,
86 scan_angle_rank: record.scan_angle,
87 point_source_id: record.point_source_id,
88 gps_time: record.gps_time,
89 red: record.red,
90 green: record.green,
91 blue: record.blue,
92 })
93 }
94}
95
96#[derive(Debug, Clone, Copy)]
97pub struct CopcWriterParams {
98 pub max_points_per_node: u32,
99 pub max_depth: u32,
100}
101
102impl Default for CopcWriterParams {
103 fn default() -> Self {
104 Self {
105 max_points_per_node: 100_000,
106 max_depth: 8,
107 }
108 }
109}
110
111pub fn write_source<S: CopcPointSource>(
112 path: &Path,
113 source: &S,
114 has_color: bool,
115 bounds: Bounds,
116 params: &CopcWriterParams,
117) -> Result<()> {
118 write_source_with_cancel(path, source, has_color, bounds, params, &NeverCancel)
119}
120
121pub fn write_source_with_cancel<S: CopcPointSource>(
122 path: &Path,
123 source: &S,
124 has_color: bool,
125 bounds: Bounds,
126 params: &CopcWriterParams,
127 cancel: &dyn CancelCheck,
128) -> Result<()> {
129 cancel.check()?;
130 if source.is_empty() {
131 return Err(Error::InvalidInput(
132 "cannot write empty cloud to COPC".into(),
133 ));
134 }
135 write_copc_inner(path, source, has_color, bounds, params, cancel)
136}
137
138pub fn write_streaming_with_cancel<I>(
139 path: &Path,
140 layout: StreamingLayout,
141 points: I,
142 params: &CopcWriterParams,
143 spill_dir: &Path,
144 cancel: &dyn CancelCheck,
145) -> Result<()>
146where
147 I: IntoIterator<Item = Result<LasPointRecord>>,
148{
149 cancel.check()?;
150 let mut spill = SpillWriter::create(spill_dir, layout)?;
151 for (index, item) in points.into_iter().enumerate() {
152 if index % CANCEL_POLL_STRIDE == 0 {
153 cancel.check()?;
154 }
155 spill.push(&item?)?;
156 }
157 cancel.check()?;
158 let reader = spill.finalize()?;
159 write_copc_from_spill(path, reader, params, cancel)
160}
161
162pub fn convert_las_to_copc_streaming(
163 las_path: &Path,
164 copc_path: &Path,
165 params: &CopcWriterParams,
166 spill_dir: &Path,
167 cancel: &dyn CancelCheck,
168) -> Result<()> {
169 cancel.check()?;
170 let mut reader = las::Reader::from_path(las_path).map_err(|e| Error::Las(e.to_string()))?;
171 let layout = StreamingLayout::from_las_format(*reader.header().point_format());
172 let mut spill = SpillWriter::create(spill_dir, layout)?;
173 for (index, result) in reader.points().enumerate() {
174 if index % CANCEL_POLL_STRIDE == 0 {
175 cancel.check()?;
176 }
177 let point = result.map_err(|e| Error::Las(e.to_string()))?;
178 let record = LasPointRecord::from_las_point(&point);
179 spill.push(&record)?;
180 }
181 cancel.check()?;
182 let reader = spill.finalize()?;
183 write_copc_from_spill(copc_path, reader, params, cancel)
184}
185
186fn write_copc_from_spill(
187 path: &Path,
188 reader: SpillReader,
189 params: &CopcWriterParams,
190 cancel: &dyn CancelCheck,
191) -> Result<()> {
192 cancel.check()?;
193 if reader.is_empty() {
194 return Err(Error::InvalidInput(
195 "cannot write empty cloud to COPC".into(),
196 ));
197 }
198 let has_color = reader.layout().has_color;
199 let bounds = reader.bounds();
200 let source = SpillSource { reader: &reader };
201 write_copc_inner(path, &source, has_color, bounds, params, cancel)
202}
203
204fn write_copc_inner<S: CopcPointSource>(
205 path: &Path,
206 source: &S,
207 has_color: bool,
208 bounds: Bounds,
209 params: &CopcWriterParams,
210 cancel: &dyn CancelCheck,
211) -> Result<()> {
212 cancel.check()?;
213 let point_format_id = if has_color { 7u8 } else { 6u8 };
214 let point_record_length = if has_color { 36u16 } else { 30u16 };
215
216 let (center, halfsize) = cube_from_bounds(&bounds);
217 let (scale_x, scale_y, scale_z) = (0.001, 0.001, 0.001);
218 let (offset_x, offset_y, offset_z) = (bounds.min.0, bounds.min.1, bounds.min.2);
219
220 let nodes = build_lod_nodes(source, center, halfsize, params, cancel)?;
221 cancel.check()?;
222
223 let var_vlr = LazVlrBuilder::default()
224 .with_point_format(point_format_id, 0)
225 .map_err(|e| Error::Las(format!("laz items: {e}")))?
226 .with_variable_chunk_size()
227 .build();
228 let mut var_vlr_bytes = Vec::new();
229 var_vlr
230 .write_to(&mut var_vlr_bytes)
231 .map_err(|e| Error::Las(format!("variable chunk LAZ VLR: {e}")))?;
232
233 let copc_info_vlr_size = 160u16;
234 let las_header_size = 375u32;
235 let total_vlr_bytes =
236 (54u32 + u32::from(copc_info_vlr_size)) + (54u32 + var_vlr_bytes.len() as u32);
237 let offset_to_point_data = las_header_size + total_vlr_bytes;
238
239 let file = File::create(path).map_err(|e| Error::io("create COPC file", e))?;
240 let mut writer = BufWriter::new(file);
241
242 let header = LasHeader {
243 point_data_format: point_format_id | 0x80,
244 point_record_length,
245 offset_to_point_data,
246 number_of_vlrs: 2,
247 scale: (scale_x, scale_y, scale_z),
248 offset: (offset_x, offset_y, offset_z),
249 bounds,
250 legacy_point_count: 0,
251 total_point_count: source.len() as u64,
252 offset_to_first_evlr: 0,
253 number_of_evlrs: 1,
254 };
255 header.write(&mut writer)?;
256
257 write_vlr_header(&mut writer, "copc", 1, copc_info_vlr_size, "COPC info")?;
258 let copc_info_payload_start = writer
259 .stream_position()
260 .map_err(|e| Error::io("record COPC info payload offset", e))?;
261 writer
262 .write_all(&[0u8; 160])
263 .map_err(|e| Error::io("write COPC info placeholder", e))?;
264
265 write_vlr_header(
266 &mut writer,
267 "laszip encoded",
268 22204,
269 var_vlr_bytes.len() as u16,
270 "http://laszip.org",
271 )?;
272 writer
273 .write_all(&var_vlr_bytes)
274 .map_err(|e| Error::io("write LAZ VLR", e))?;
275
276 let point_data_actual_start = writer
277 .stream_position()
278 .map_err(|e| Error::io("record point data offset", e))?;
279 if point_data_actual_start as u32 != offset_to_point_data {
280 return Err(Error::InvalidInput(format!(
281 "VLR size accounting mismatch: at {point_data_actual_start}, expected {offset_to_point_data}"
282 )));
283 }
284
285 let mut compressor = LasZipCompressor::new(&mut writer, var_vlr.clone())
286 .map_err(|e| Error::Las(format!("compressor: {e}")))?;
287 let mut hierarchy: Vec<Entry> = Vec::with_capacity(nodes.len());
288 let mut point_buf = vec![0u8; point_record_length as usize];
289 let mut chunk_start_file_offset = compressor
290 .get_mut()
291 .stream_position()
292 .map_err(|e| Error::io("record chunk start", e))?;
293 chunk_start_file_offset += 8;
294
295 for (key, indices) in &nodes {
296 cancel.check()?;
297 for (point_index, &source_index) in indices.iter().enumerate() {
298 if point_index % CANCEL_POLL_STRIDE == 0 {
299 cancel.check()?;
300 }
301 let fields = source.fields(source_index as usize)?;
302 encode_point_record(
303 &mut point_buf,
304 &fields,
305 (scale_x, scale_y, scale_z),
306 (offset_x, offset_y, offset_z),
307 point_format_id,
308 has_color,
309 )?;
310 compressor
311 .compress_one(&point_buf)
312 .map_err(|e| Error::Las(format!("compress point: {e}")))?;
313 }
314 compressor
315 .finish_current_chunk()
316 .map_err(|e| Error::Las(format!("finish chunk: {e}")))?;
317 let after = compressor
318 .get_mut()
319 .stream_position()
320 .map_err(|e| Error::io("record chunk end", e))?;
321 hierarchy.push(Entry {
322 key: *key,
323 offset: chunk_start_file_offset,
324 byte_size: (after - chunk_start_file_offset) as i32,
325 point_count: indices.len() as i32,
326 });
327 chunk_start_file_offset = after;
328 }
329
330 cancel.check()?;
331 compressor
332 .done()
333 .map_err(|e| Error::Las(format!("finish compressor: {e}")))?;
334 drop(compressor);
335
336 let evlr_start = writer
337 .stream_position()
338 .map_err(|e| Error::io("record EVLR start", e))?;
339 let hierarchy_body_size = (hierarchy.len() * 32) as u64;
340 write_evlr_header(
341 &mut writer,
342 "copc",
343 1000,
344 hierarchy_body_size,
345 "COPC hierarchy",
346 )?;
347 let root_hier_offset = writer
348 .stream_position()
349 .map_err(|e| Error::io("record root hierarchy offset", e))?;
350 let mut entry_buf = [0u8; 32];
351 for entry in &hierarchy {
352 entry.write_le(&mut entry_buf)?;
353 writer
354 .write_all(&entry_buf)
355 .map_err(|e| Error::io("write hierarchy entry", e))?;
356 }
357
358 writer
359 .seek(SeekFrom::Start(copc_info_payload_start))
360 .map_err(|e| Error::io("seek COPC info payload", e))?;
361 let info = CopcInfo {
362 center,
363 halfsize,
364 spacing: halfsize / 128.0,
365 root_hier_offset,
366 root_hier_size: hierarchy_body_size,
367 gpstime_min: 0.0,
368 gpstime_max: 0.0,
369 };
370 writer
371 .write_all(&info.write_le_bytes())
372 .map_err(|e| Error::io("patch COPC info", e))?;
373
374 writer
375 .seek(SeekFrom::Start(235))
376 .map_err(|e| Error::io("seek first EVLR offset", e))?;
377 writer
378 .write_u64::<LittleEndian>(evlr_start)
379 .map_err(|e| Error::io("patch first EVLR offset", e))?;
380
381 writer
382 .flush()
383 .map_err(|e| Error::io("flush COPC file", e))?;
384 Ok(())
385}
386
387fn build_lod_nodes<S: CopcPointSource>(
388 source: &S,
389 center: (f64, f64, f64),
390 halfsize: f64,
391 params: &CopcWriterParams,
392 cancel: &dyn CancelCheck,
393) -> Result<Vec<(VoxelKey, Vec<u32>)>> {
394 cancel.check()?;
395 let total_points = u32::try_from(source.len()).map_err(|_| {
396 Error::InvalidInput("COPC writer supports at most u32::MAX points per file".into())
397 })?;
398 let max_points_per_node = params.max_points_per_node.max(1) as usize;
399 let max_depth = params.max_depth.min(30);
400 let mut builder = LodNodeBuilder {
401 source,
402 max_points_per_node,
403 max_depth,
404 cancel,
405 nodes: Vec::new(),
406 };
407 builder.assign(
408 VoxelKey::root(),
409 (0..total_points).collect(),
410 Bounds::cube(center, halfsize),
411 )?;
412 let mut nodes = builder.nodes;
413 nodes.sort_by_key(|(key, _)| *key);
414 Ok(nodes)
415}
416
417struct LodNodeBuilder<'a, S: CopcPointSource> {
418 source: &'a S,
419 max_points_per_node: usize,
420 max_depth: u32,
421 cancel: &'a dyn CancelCheck,
422 nodes: Vec<(VoxelKey, Vec<u32>)>,
423}
424
425impl<S: CopcPointSource> LodNodeBuilder<'_, S> {
426 fn assign(&mut self, key: VoxelKey, indices: Vec<u32>, bounds: Bounds) -> Result<()> {
427 self.cancel.check()?;
428 if indices.is_empty() {
429 return Ok(());
430 }
431 if indices.len() <= self.max_points_per_node || key.level as u32 >= self.max_depth {
432 self.nodes.push((key, indices));
433 return Ok(());
434 }
435
436 let mut children: [Vec<u32>; 8] = std::array::from_fn(|_| Vec::new());
437 for (partition_index, index) in indices.into_iter().enumerate() {
438 if partition_index % 16_384 == 0 {
439 self.cancel.check()?;
440 }
441 let (px, py, pz) = self.source.xyz(index as usize);
442 children[child_octant(bounds, px, py, pz)].push(index);
443 }
444 for child in &mut children {
445 child.reverse();
446 }
447
448 let mut selected = Vec::with_capacity(self.max_points_per_node);
449 while selected.len() < self.max_points_per_node {
450 let mut progressed = false;
451 for child in &mut children {
452 if let Some(index) = child.pop() {
453 selected.push(index);
454 progressed = true;
455 if selected.len() == self.max_points_per_node {
456 break;
457 }
458 }
459 }
460 if !progressed {
461 break;
462 }
463 }
464 self.nodes.push((key, selected));
465
466 for (octant, child_indices) in children.into_iter().enumerate() {
467 if child_indices.is_empty() {
468 continue;
469 }
470 self.assign(
471 key.child(octant as u8),
472 child_indices,
473 bounds.octant(octant as u8),
474 )?;
475 }
476 Ok(())
477 }
478}
479
480fn child_octant(bounds: Bounds, x: f64, y: f64, z: f64) -> usize {
481 let center = bounds.center();
482 usize::from(x >= center.0)
483 | (usize::from(y >= center.1) << 1)
484 | (usize::from(z >= center.2) << 2)
485}
486
487fn cube_from_bounds(bounds: &Bounds) -> ((f64, f64, f64), f64) {
488 let center = bounds.center();
489 let dx = bounds.max.0 - bounds.min.0;
490 let dy = bounds.max.1 - bounds.min.1;
491 let dz = bounds.max.2 - bounds.min.2;
492 let halfsize = (dx.max(dy).max(dz) * 0.5).max(1e-6);
493 (center, halfsize)
494}
495
496struct LasHeader {
497 point_data_format: u8,
498 point_record_length: u16,
499 offset_to_point_data: u32,
500 number_of_vlrs: u32,
501 scale: (f64, f64, f64),
502 offset: (f64, f64, f64),
503 bounds: Bounds,
504 legacy_point_count: u32,
505 total_point_count: u64,
506 offset_to_first_evlr: u64,
507 number_of_evlrs: u32,
508}
509
510impl LasHeader {
511 fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
512 writer
513 .write_all(b"LASF")
514 .map_err(|e| Error::io("write LAS signature", e))?;
515 writer
516 .write_u16::<LittleEndian>(0)
517 .map_err(|e| Error::io("write file source id", e))?;
518 writer
519 .write_u16::<LittleEndian>(0)
520 .map_err(|e| Error::io("write global encoding", e))?;
521 writer
522 .write_u32::<LittleEndian>(0)
523 .map_err(|e| Error::io("write GUID1", e))?;
524 writer
525 .write_u16::<LittleEndian>(0)
526 .map_err(|e| Error::io("write GUID2", e))?;
527 writer
528 .write_u16::<LittleEndian>(0)
529 .map_err(|e| Error::io("write GUID3", e))?;
530 writer
531 .write_all(&[0u8; 8])
532 .map_err(|e| Error::io("write GUID4", e))?;
533 writer
534 .write_u8(1)
535 .map_err(|e| Error::io("write version major", e))?;
536 writer
537 .write_u8(4)
538 .map_err(|e| Error::io("write version minor", e))?;
539 writer
540 .write_all(&pad(b"copc-rust", 32))
541 .map_err(|e| Error::io("write system id", e))?;
542 writer
543 .write_all(&pad(b"copc-writer", 32))
544 .map_err(|e| Error::io("write generating software", e))?;
545 writer
546 .write_u16::<LittleEndian>(0)
547 .map_err(|e| Error::io("write creation day", e))?;
548 writer
549 .write_u16::<LittleEndian>(2026)
550 .map_err(|e| Error::io("write creation year", e))?;
551 writer
552 .write_u16::<LittleEndian>(375)
553 .map_err(|e| Error::io("write header size", e))?;
554 writer
555 .write_u32::<LittleEndian>(self.offset_to_point_data)
556 .map_err(|e| Error::io("write point data offset", e))?;
557 writer
558 .write_u32::<LittleEndian>(self.number_of_vlrs)
559 .map_err(|e| Error::io("write VLR count", e))?;
560 writer
561 .write_u8(self.point_data_format)
562 .map_err(|e| Error::io("write point format", e))?;
563 writer
564 .write_u16::<LittleEndian>(self.point_record_length)
565 .map_err(|e| Error::io("write point record length", e))?;
566 writer
567 .write_u32::<LittleEndian>(self.legacy_point_count)
568 .map_err(|e| Error::io("write legacy point count", e))?;
569 for _ in 0..5 {
570 writer
571 .write_u32::<LittleEndian>(0)
572 .map_err(|e| Error::io("write legacy returns", e))?;
573 }
574 writer
575 .write_f64::<LittleEndian>(self.scale.0)
576 .map_err(|e| Error::io("write x scale", e))?;
577 writer
578 .write_f64::<LittleEndian>(self.scale.1)
579 .map_err(|e| Error::io("write y scale", e))?;
580 writer
581 .write_f64::<LittleEndian>(self.scale.2)
582 .map_err(|e| Error::io("write z scale", e))?;
583 writer
584 .write_f64::<LittleEndian>(self.offset.0)
585 .map_err(|e| Error::io("write x offset", e))?;
586 writer
587 .write_f64::<LittleEndian>(self.offset.1)
588 .map_err(|e| Error::io("write y offset", e))?;
589 writer
590 .write_f64::<LittleEndian>(self.offset.2)
591 .map_err(|e| Error::io("write z offset", e))?;
592 writer
593 .write_f64::<LittleEndian>(self.bounds.max.0)
594 .map_err(|e| Error::io("write max x", e))?;
595 writer
596 .write_f64::<LittleEndian>(self.bounds.min.0)
597 .map_err(|e| Error::io("write min x", e))?;
598 writer
599 .write_f64::<LittleEndian>(self.bounds.max.1)
600 .map_err(|e| Error::io("write max y", e))?;
601 writer
602 .write_f64::<LittleEndian>(self.bounds.min.1)
603 .map_err(|e| Error::io("write min y", e))?;
604 writer
605 .write_f64::<LittleEndian>(self.bounds.max.2)
606 .map_err(|e| Error::io("write max z", e))?;
607 writer
608 .write_f64::<LittleEndian>(self.bounds.min.2)
609 .map_err(|e| Error::io("write min z", e))?;
610 writer
611 .write_u64::<LittleEndian>(0)
612 .map_err(|e| Error::io("write waveform packet start", e))?;
613 writer
614 .write_u64::<LittleEndian>(self.offset_to_first_evlr)
615 .map_err(|e| Error::io("write first EVLR offset", e))?;
616 writer
617 .write_u32::<LittleEndian>(self.number_of_evlrs)
618 .map_err(|e| Error::io("write EVLR count", e))?;
619 writer
620 .write_u64::<LittleEndian>(self.total_point_count)
621 .map_err(|e| Error::io("write total point count", e))?;
622 for _ in 0..15 {
623 writer
624 .write_u64::<LittleEndian>(0)
625 .map_err(|e| Error::io("write extended returns", e))?;
626 }
627 Ok(())
628 }
629}
630
631fn pad(value: &[u8], len: usize) -> Vec<u8> {
632 let mut out = Vec::with_capacity(len);
633 let take = value.len().min(len);
634 out.extend_from_slice(&value[..take]);
635 out.resize(len, 0);
636 out
637}
638
639fn write_vlr_header<W: Write>(
640 writer: &mut W,
641 user_id: &str,
642 record_id: u16,
643 body_size: u16,
644 description: &str,
645) -> Result<()> {
646 writer
647 .write_u16::<LittleEndian>(0)
648 .map_err(|e| Error::io("write VLR reserved", e))?;
649 writer
650 .write_all(&pad(user_id.as_bytes(), 16))
651 .map_err(|e| Error::io("write VLR user id", e))?;
652 writer
653 .write_u16::<LittleEndian>(record_id)
654 .map_err(|e| Error::io("write VLR record id", e))?;
655 writer
656 .write_u16::<LittleEndian>(body_size)
657 .map_err(|e| Error::io("write VLR body size", e))?;
658 writer
659 .write_all(&pad(description.as_bytes(), 32))
660 .map_err(|e| Error::io("write VLR description", e))?;
661 Ok(())
662}
663
664fn write_evlr_header<W: Write>(
665 writer: &mut W,
666 user_id: &str,
667 record_id: u16,
668 body_size: u64,
669 description: &str,
670) -> Result<()> {
671 writer
672 .write_u16::<LittleEndian>(0)
673 .map_err(|e| Error::io("write EVLR reserved", e))?;
674 writer
675 .write_all(&pad(user_id.as_bytes(), 16))
676 .map_err(|e| Error::io("write EVLR user id", e))?;
677 writer
678 .write_u16::<LittleEndian>(record_id)
679 .map_err(|e| Error::io("write EVLR record id", e))?;
680 writer
681 .write_u64::<LittleEndian>(body_size)
682 .map_err(|e| Error::io("write EVLR body size", e))?;
683 writer
684 .write_all(&pad(description.as_bytes(), 32))
685 .map_err(|e| Error::io("write EVLR description", e))?;
686 Ok(())
687}
688
689fn encode_point_record(
690 buf: &mut [u8],
691 fields: &CopcPointFields,
692 scale: (f64, f64, f64),
693 offset: (f64, f64, f64),
694 format_id: u8,
695 has_color: bool,
696) -> Result<()> {
697 let mut cursor = Cursor::new(buf);
698 let ix = ((fields.x - offset.0) / scale.0).round() as i32;
699 let iy = ((fields.y - offset.1) / scale.1).round() as i32;
700 let iz = ((fields.z - offset.2) / scale.2).round() as i32;
701 cursor
702 .write_i32::<LittleEndian>(ix)
703 .map_err(|e| Error::io("write point x", e))?;
704 cursor
705 .write_i32::<LittleEndian>(iy)
706 .map_err(|e| Error::io("write point y", e))?;
707 cursor
708 .write_i32::<LittleEndian>(iz)
709 .map_err(|e| Error::io("write point z", e))?;
710 cursor
711 .write_u16::<LittleEndian>(fields.intensity)
712 .map_err(|e| Error::io("write intensity", e))?;
713 let rn = fields.return_number & 0x0F;
714 let nr = fields.number_of_returns & 0x0F;
715 cursor
716 .write_u8(rn | (nr << 4))
717 .map_err(|e| Error::io("write return flags", e))?;
718 let flags = (fields.synthetic & 1)
719 | ((fields.key_point & 1) << 1)
720 | ((fields.withheld & 1) << 2)
721 | ((fields.overlap & 1) << 3);
722 let chan = fields.scan_channel & 0x03;
723 let sd = fields.scan_direction_flag & 1;
724 let eof = fields.edge_of_flight_line & 1;
725 cursor
726 .write_u8(flags | (chan << 4) | (sd << 6) | (eof << 7))
727 .map_err(|e| Error::io("write classification flags", e))?;
728 cursor
729 .write_u8(fields.classification)
730 .map_err(|e| Error::io("write classification", e))?;
731 cursor
732 .write_u8(fields.user_data)
733 .map_err(|e| Error::io("write user data", e))?;
734 let scan_angle = (fields.scan_angle_rank as f32 / 0.006) as i16;
735 cursor
736 .write_i16::<LittleEndian>(scan_angle)
737 .map_err(|e| Error::io("write scan angle", e))?;
738 cursor
739 .write_u16::<LittleEndian>(fields.point_source_id)
740 .map_err(|e| Error::io("write point source id", e))?;
741 cursor
742 .write_f64::<LittleEndian>(fields.gps_time)
743 .map_err(|e| Error::io("write gps time", e))?;
744 if format_id == 7 && has_color {
745 cursor
746 .write_u16::<LittleEndian>(fields.red)
747 .map_err(|e| Error::io("write red", e))?;
748 cursor
749 .write_u16::<LittleEndian>(fields.green)
750 .map_err(|e| Error::io("write green", e))?;
751 cursor
752 .write_u16::<LittleEndian>(fields.blue)
753 .map_err(|e| Error::io("write blue", e))?;
754 }
755 Ok(())
756}