1use std::io::{BufRead, Read as _};
9use std::path::Path;
10
11use indexmap::IndexMap;
12
13use crate::bbi::header::{BbiKind, BED_FIELD_NAMES};
14use crate::bbi::writer::{BbiWriter, BbiWriterOptions};
15use crate::error::{Error, Result};
16use crate::genomic::ChrMap;
17use crate::progress::{CancelFlag, ProgressFn, ProgressTracker};
18
19const VALUE_BATCH: usize = 65536;
21const PROGRESS_INTERVAL: u64 = 65536;
23const MAX_LINE_SIZE: usize = 16 << 20;
25
26const ORDER_HINT: &str = "input must be pooled by chromosome and sorted by start, \
31 eg with `sort -k1,1 -k2,2n`";
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum TextFormat {
35 BedGraph,
36 Wig,
37 Bed,
38}
39
40impl TextFormat {
41 pub fn as_str(self) -> &'static str {
42 match self {
43 TextFormat::BedGraph => "bedgraph",
44 TextFormat::Wig => "wig",
45 TextFormat::Bed => "bed",
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
51pub struct ConvertResult {
52 pub format: TextFormat,
53 pub line_count: u64,
54 pub item_count: u64,
55 pub skipped_count: u64,
56 pub clipped_count: u64,
57 pub chr_sizes: Vec<(String, i64)>,
58}
59
60struct LineReader {
62 input: crate::source::TextInput,
63 path: String,
64 size: u64,
65 consumed: u64,
66 line_number: u64,
67 buffer: Vec<u8>,
68}
69
70impl LineReader {
71 fn open(path: &Path) -> Result<Self> {
72 let (input, size) = crate::source::open_text(path)?;
73 Ok(Self {
74 input,
75 path: path.to_string_lossy().into_owned(),
76 size,
77 consumed: 0,
78 line_number: 0,
79 buffer: Vec::with_capacity(4096),
80 })
81 }
82
83 fn read_line_into(&mut self, out: &mut String) -> Result<bool> {
95 let Some(line) = self.next_line()? else {
96 return Ok(false);
97 };
98 out.clear();
102 out.push_str(line);
103 Ok(true)
104 }
105
106 fn next_line(&mut self) -> Result<Option<&str>> {
107 self.buffer.clear();
108 let read = self
113 .input
114 .by_ref()
115 .take(MAX_LINE_SIZE as u64 + 1)
116 .read_until(b'\n', &mut self.buffer)
117 .map_err(|e| Error::io(&self.path, e))?;
118 if read == 0 {
119 return Ok(None);
120 }
121 self.consumed += read as u64;
123 self.line_number += 1;
124 if self.buffer.len() > MAX_LINE_SIZE {
125 return Err(Error::format(
126 &self.path,
127 format!(
128 "line {} is longer than {MAX_LINE_SIZE} bytes",
129 self.line_number
130 ),
131 ));
132 }
133 while matches!(self.buffer.last(), Some(b'\n' | b'\r')) {
134 self.buffer.pop();
135 }
136 if std::str::from_utf8(&self.buffer).is_err() {
150 self.buffer = String::from_utf8_lossy(&self.buffer)
151 .into_owned()
152 .into_bytes();
153 }
154 Ok(Some(
155 std::str::from_utf8(&self.buffer).expect("the buffer above is valid utf-8 or replaced"),
156 ))
157 }
158
159 fn fail(&self, message: impl std::fmt::Display) -> Error {
174 Error::format(&self.path, format!("line {}: {message}", self.line_number))
175 }
176
177 fn guard(&self, message: impl std::fmt::Display) -> Error {
180 Error::format(
181 &self.path,
182 format!("line {}: {message}\n{ORDER_HINT}", self.line_number),
183 )
184 }
185}
186
187fn is_blank(c: char) -> bool {
190 c == ' ' || c == '\t'
191}
192
193fn split_blanks(line: &str) -> Vec<&str> {
199 line.split(is_blank).filter(|f| !f.is_empty()).collect()
200}
201
202fn split_tabs(line: &str) -> Vec<&str> {
208 line.split('\t').collect()
209}
210
211fn trim_trailing_tab(fields: &mut Vec<&str>, expected: Option<usize>) {
225 if fields.len() < 2 || !fields.last().is_some_and(|f| f.is_empty()) {
226 return;
227 }
228 match expected {
229 None => {
232 fields.pop();
233 }
234 Some(width) if fields.len() == width + 1 => {
235 fields.pop();
236 }
237 Some(_) => {}
238 }
239}
240
241fn starts_with_token(line: &str, token: &str) -> bool {
244 let bytes = line.as_bytes();
245 let token = token.as_bytes();
246 if bytes.len() < token.len() {
247 return false;
248 }
249 if !bytes[..token.len()].eq_ignore_ascii_case(token) {
250 return false;
251 }
252 bytes.len() == token.len() || bytes[token.len()] == b' ' || bytes[token.len()] == b'\t'
253}
254
255fn is_skipped_line(line: &str) -> bool {
258 let trimmed = line.trim_start_matches(is_blank);
259 trimmed.is_empty()
260 || trimmed.starts_with('#')
261 || starts_with_token(trimmed, "track")
262 || starts_with_token(trimmed, "browser")
263}
264
265fn is_declaration(field: &str) -> bool {
266 starts_with_token(field, "fixedstep") || starts_with_token(field, "variablestep")
267}
268
269fn is_bedgraph_record(fields: &[&str]) -> bool {
276 fields[1].parse::<i64>().is_ok()
277 && fields[2].parse::<i64>().is_ok()
278 && fields[3].parse::<f64>().is_ok()
279}
280
281fn is_orphan_wig_data(fields: &[&str]) -> bool {
287 !fields.is_empty() && fields.len() <= 2 && fields.iter().all(|f| f.parse::<f64>().is_ok())
288}
289
290pub fn sniff_format(first_data_line: &str) -> Result<TextFormat> {
296 let fields = split_blanks(first_data_line);
297 if fields.first().is_some_and(|f| is_declaration(f)) {
298 return Ok(TextFormat::Wig);
299 }
300 if fields.len() == 4 && is_bedgraph_record(&fields) {
301 return Ok(TextFormat::BedGraph);
302 }
303 if is_orphan_wig_data(&fields) {
304 return Err(Error::invalid(
305 "wig data before any fixedStep or variableStep declaration",
306 ));
307 }
308 Err(Error::invalid(format!(
309 "\"{first_data_line}\" is neither a bedgraph record (chr, start, end, value) \
310 nor a wig declaration (fixedStep or variableStep), so the format of the input \
311 cannot be told"
312 )))
313}
314
315#[derive(Debug, Clone, Default)]
324pub struct WigDeclaration {
325 pub fixed_step: bool,
326 pub chr: String,
327 pub start: i64,
330 pub step: i64,
331 pub span: i64,
332}
333
334pub fn parse_wig_declaration(line: &str) -> Result<WigDeclaration> {
335 let fields = split_blanks(line);
336 let mut declaration = WigDeclaration {
337 fixed_step: fields
338 .first()
339 .is_some_and(|f| starts_with_token(f, "fixedstep")),
340 step: 1,
341 span: 1,
342 ..Default::default()
343 };
344 let (mut has_chr, mut has_start) = (false, false);
345 for field in &fields[1..] {
346 let Some((key, value)) = field.split_once('=') else {
347 return Err(Error::invalid(format!(
348 "\"{field}\" is not a key=value of a wig declaration"
349 )));
350 };
351 let number = |what: &str| -> Result<i64> {
352 value
353 .parse::<i64>()
354 .map_err(|_| Error::invalid(format!("could not read \"{value}\" as a {what}")))
355 };
356 match key.to_ascii_lowercase().as_str() {
357 "chrom" => {
358 declaration.chr = value.to_string();
359 has_chr = true;
360 }
361 "start" => {
362 let start = number("start")?;
363 if start < 1 {
364 return Err(Error::invalid(format!(
365 "start {start} is not a 1-based coordinate"
366 )));
367 }
368 declaration.start = start - 1;
369 has_start = true;
370 }
371 "step" => declaration.step = number("step")?,
372 "span" => declaration.span = number("span")?,
373 other => {
374 return Err(Error::invalid(format!(
375 "{other} is not a wig declaration key (chrom, start, step, span)"
376 )))
377 }
378 }
379 }
380 if !has_chr {
381 return Err(Error::invalid("wig declaration has no chrom"));
382 }
383 if declaration.fixed_step && !has_start {
384 return Err(Error::invalid("fixedStep declaration has no start"));
385 }
386 if declaration.step <= 0 {
387 return Err(Error::invalid(format!(
388 "step {} must be positive",
389 declaration.step
390 )));
391 }
392 if declaration.span <= 0 {
393 return Err(Error::invalid(format!(
394 "span {} must be positive",
395 declaration.span
396 )));
397 }
398 Ok(declaration)
399}
400
401struct ValueSink {
413 bin_size: i64,
414 declared: Option<ChrMap>,
415 chr: String,
416 bin: Option<i64>,
418 bin_sum: f64,
419 bin_covered: i64,
420 run_start_bin: Option<i64>,
421 run_values: Vec<f32>,
422 item_count: u64,
423 clipped_count: u64,
424 last_end: i64,
427 chr_size: Option<i64>,
428}
429
430impl ValueSink {
431 fn new(bin_size: i64, declared: Option<ChrMap>) -> Self {
432 Self {
433 bin_size,
434 declared,
435 chr: String::new(),
436 bin: None,
437 bin_sum: 0.0,
438 bin_covered: 0,
439 run_start_bin: None,
440 run_values: Vec::new(),
441 item_count: 0,
442 clipped_count: 0,
443 last_end: 0,
444 chr_size: None,
445 }
446 }
447
448 fn binning(&self) -> bool {
449 self.bin_size > 0
450 }
451
452 fn set_chr(&mut self, writer: &mut BbiWriter, chr: &str) -> Result<()> {
459 if self.chr == chr {
460 return Ok(());
461 }
462 self.finish(writer)?;
463 self.chr.clear();
464 self.chr.push_str(chr);
465 self.last_end = 0;
466 self.chr_size = None;
467 if let Some(declared) = &self.declared {
468 self.chr_size = Some(declared.resolve(chr)?.size);
469 }
470 Ok(())
471 }
472
473 fn add(
474 &mut self,
475 writer: &mut BbiWriter,
476 chr: &str,
477 start: i64,
478 end: i64,
479 value: f32,
480 ) -> Result<()> {
481 self.set_chr(writer, chr)?;
482 self.item_count += 1;
483 if !self.binning() {
484 let chr = std::mem::take(&mut self.chr);
485 let result = writer.write_value(&chr, start, end, value);
486 self.chr = chr;
487 return result;
488 }
489 self.accumulate(writer, start, end, value)
490 }
491
492 fn add_run(
493 &mut self,
494 writer: &mut BbiWriter,
495 chr: &str,
496 start: i64,
497 span: i64,
498 values: &[f32],
499 ) -> Result<()> {
500 if values.is_empty() {
501 return Ok(());
502 }
503 self.set_chr(writer, chr)?;
504 self.item_count += values.len() as u64;
505 if !self.binning() {
506 let chr = std::mem::take(&mut self.chr);
507 let result = writer.write_values(&chr, start, span, values);
508 self.chr = chr;
509 return result;
510 }
511 for (i, value) in values.iter().enumerate() {
512 let i = i as i64;
513 self.accumulate(writer, start + span * i, start + span * (i + 1), *value)?;
514 }
515 Ok(())
516 }
517
518 fn accumulate(
520 &mut self,
521 writer: &mut BbiWriter,
522 start: i64,
523 mut end: i64,
524 value: f32,
525 ) -> Result<()> {
526 if start < 0 {
527 return Err(Error::invalid(format!("start {start} is negative")));
528 }
529 if end <= start {
530 return Err(Error::invalid(format!(
531 "end {end} is not past the start {start}"
532 )));
533 }
534 if start < self.last_end {
535 return Err(Error::invalid(format!(
536 "{}:{start}-{end} starts before the end {} of the previous value, values \
537 must be added in order and without overlap",
538 self.chr, self.last_end
539 )));
540 }
541 if let Some(size) = self.chr_size {
542 if end > size {
543 if start >= size {
544 return Err(Error::invalid(format!(
545 "{}:{start}-{end} starts past the end of {}, which is {size} bases long",
546 self.chr, self.chr
547 )));
548 }
549 self.clipped_count += 1;
550 end = size;
551 }
552 }
553 self.last_end = end;
554 let mut index = start / self.bin_size;
555 while index * self.bin_size < end {
556 if Some(index) != self.bin {
557 self.close_bin(writer)?;
558 self.bin = Some(index);
559 self.bin_sum = 0.0;
560 self.bin_covered = 0;
561 }
562 let overlap = end.min((index + 1) * self.bin_size) - start.max(index * self.bin_size);
563 self.bin_sum += value as f64 * overlap as f64;
564 self.bin_covered += overlap;
565 index += 1;
566 }
567 Ok(())
568 }
569
570 fn close_bin(&mut self, writer: &mut BbiWriter) -> Result<()> {
572 let Some(index) = self.bin.take() else {
573 return Ok(());
574 };
575 let (covered, sum) = (self.bin_covered, self.bin_sum);
576 self.bin_sum = 0.0;
577 self.bin_covered = 0;
578 if covered <= 0 {
579 return Ok(());
580 }
581 let value = (sum / covered as f64) as f32;
582 if self
583 .run_start_bin
584 .is_some_and(|first| index != first + self.run_values.len() as i64)
585 {
586 self.flush_run(writer)?;
587 }
588 if self.run_start_bin.is_none() {
589 self.run_start_bin = Some(index);
590 }
591 self.run_values.push(value);
592 if self.run_values.len() >= VALUE_BATCH {
593 self.flush_run(writer)?;
594 }
595 Ok(())
596 }
597
598 fn flush_run(&mut self, writer: &mut BbiWriter) -> Result<()> {
599 let Some(first) = self.run_start_bin.take() else {
600 return Ok(());
601 };
602 if self.run_values.is_empty() {
603 return Ok(());
604 }
605 let start = first * self.bin_size;
606 let values = std::mem::take(&mut self.run_values);
607 let chr = std::mem::take(&mut self.chr);
608 let result = writer.write_values(&chr, start, self.bin_size, &values);
609 self.chr = chr;
610 self.run_values = values;
611 self.run_values.clear();
612 result
613 }
614
615 fn finish(&mut self, writer: &mut BbiWriter) -> Result<()> {
617 self.close_bin(writer)?;
618 self.flush_run(writer)
619 }
620}
621
622pub fn convert_to_bigwig(
631 input: &Path,
632 output: &Path,
633 bin_size: Option<i64>,
634 mut options: BbiWriterOptions,
635 progress: Option<ProgressFn>,
636 cancel: Option<CancelFlag>,
637) -> Result<ConvertResult> {
638 let bin_size = bin_size.unwrap_or(0);
639 if bin_size < 0 {
640 return Err(Error::invalid(format!(
641 "bin_size {bin_size} must not be negative"
642 )));
643 }
644 options.kind = BbiKind::BigWig;
645 let declared = options.chr_sizes.clone();
646
647 let mut reader = LineReader::open(input)?;
648 let mut writer = BbiWriter::create(&output.to_string_lossy(), options)?;
649 let mut sink = ValueSink::new(bin_size, declared);
650 let tracker = ProgressTracker::with_callback(reader.size, progress);
651
652 let mut format: Option<TextFormat> = None;
653 let mut line_count = 0u64;
654 let mut declaration = WigDeclaration::default();
655 let mut declared_yet = false;
656 let mut run: Vec<f32> = Vec::new();
660 let mut run_start = 0i64;
661 let mut reported = 0u64;
662
663 let mut line = String::new();
668 let result = (|| -> Result<()> {
669 while reader.read_line_into(&mut line)? {
670 line_count += 1;
671 if line_count % PROGRESS_INTERVAL == 0 {
672 tracker.add(reader.consumed - reported);
673 reported = reader.consumed;
674 if cancel.as_ref().is_some_and(CancelFlag::is_cancelled) {
678 return Err(Error::invalid("conversion cancelled"));
679 }
680 }
681 if is_skipped_line(&line) {
682 continue;
683 }
684 let fields = split_blanks(&line);
685 if fields.is_empty() {
686 continue;
687 }
688 let declaration_line = is_declaration(fields[0]);
689
690 if format.is_none() {
691 format = Some(sniff_format(&line).map_err(|e| reader.fail(e))?);
692 }
693
694 if format == Some(TextFormat::Wig) {
695 if declaration_line {
696 flush_run(&mut sink, &mut writer, &declaration, run_start, &mut run)
697 .map_err(|e| reader.guard(e))?;
698 declaration = parse_wig_declaration(&line).map_err(|e| reader.fail(e))?;
699 declared_yet = true;
700 continue;
701 }
702 if !declared_yet {
703 return Err(
704 reader.fail("wig data before any fixedStep or variableStep declaration")
705 );
706 }
707 if declaration.fixed_step {
708 if fields.len() != 1 {
709 return Err(reader.fail(format!(
710 "fixedStep data has {} columns, not 1",
711 fields.len()
712 )));
713 }
714 let value = parse_f32(fields[0]).map_err(|e| reader.fail(e))?;
715 if declaration.step == declaration.span {
719 if run.is_empty() {
720 run_start = declaration.start;
721 }
722 run.push(value);
723 if run.len() >= VALUE_BATCH {
724 flush_run(&mut sink, &mut writer, &declaration, run_start, &mut run)
725 .map_err(|e| reader.guard(e))?;
726 }
727 } else {
728 sink.add(
729 &mut writer,
730 &declaration.chr,
731 declaration.start,
732 declaration.start + declaration.span,
733 value,
734 )
735 .map_err(|e| reader.guard(e))?;
736 }
737 declaration.start += declaration.step;
738 } else {
739 if fields.len() != 2 {
740 return Err(reader.fail(format!(
741 "variableStep data has {} columns, not 2",
742 fields.len()
743 )));
744 }
745 let start = parse_i64(fields[0], "position").map_err(|e| reader.fail(e))?;
746 let value = parse_f32(fields[1]).map_err(|e| reader.fail(e))?;
747 if start < 1 {
748 return Err(reader.fail(format!("position {start} is not 1-based")));
749 }
750 sink.add(
751 &mut writer,
752 &declaration.chr,
753 start - 1,
754 start - 1 + declaration.span,
755 value,
756 )
757 .map_err(|e| reader.guard(e))?;
758 }
759 continue;
760 }
761
762 if declaration_line {
763 return Err(
764 reader.fail("wig declaration in what has been read as a bedgraph so far")
765 );
766 }
767 if fields.len() != 4 {
768 return Err(reader.fail(format!(
769 "bedgraph record has {} columns, not 4",
770 fields.len()
771 )));
772 }
773 let start = parse_i64(fields[1], "start").map_err(|e| reader.fail(e))?;
774 let end = parse_i64(fields[2], "end").map_err(|e| reader.fail(e))?;
775 let value = parse_f32(fields[3]).map_err(|e| reader.fail(e))?;
776 sink.add(&mut writer, fields[0], start, end, value)
777 .map_err(|e| reader.guard(e))?;
778 }
779
780 flush_run(&mut sink, &mut writer, &declaration, run_start, &mut run)
781 .map_err(|e| reader.guard(e))?;
782 sink.finish(&mut writer).map_err(|e| reader.guard(e))?;
783 writer.close()
784 })();
785
786 if let Err(error) = result {
787 writer.abandon();
788 return Err(error);
789 }
790 tracker.done_report();
791
792 Ok(ConvertResult {
793 format: format.unwrap_or(TextFormat::BedGraph),
796 line_count,
797 item_count: sink.item_count,
798 skipped_count: writer.skipped_count(),
799 clipped_count: writer.clipped_count() + sink.clipped_count,
803 chr_sizes: writer.chr_sizes(),
804 })
805}
806
807fn flush_run(
808 sink: &mut ValueSink,
809 writer: &mut BbiWriter,
810 declaration: &WigDeclaration,
811 run_start: i64,
812 run: &mut Vec<f32>,
813) -> Result<()> {
814 if run.is_empty() {
815 return Ok(());
816 }
817 let result = sink.add_run(writer, &declaration.chr, run_start, declaration.span, run);
818 run.clear();
819 result
820}
821
822fn parse_i64(text: &str, what: &str) -> Result<i64> {
823 text.parse()
824 .map_err(|_| Error::invalid(format!("could not read \"{text}\" as a {what}")))
825}
826
827fn parse_f32(text: &str) -> Result<f32> {
828 text.parse::<f64>()
829 .map(|v| v as f32)
830 .map_err(|_| Error::invalid(format!("could not read \"{text}\" as a number")))
831}
832
833const BED_FIELD_STANDARD_TYPES: &[&str] = &[
841 "string", "uint", "uint", "string", "uint", "string", "uint", "uint", "string", "uint",
842 "string", "string",
843];
844
845pub fn default_bed_fields(col_count: usize) -> IndexMap<String, String> {
852 (0..col_count)
853 .map(|index| {
854 if index < BED_FIELD_NAMES.len() {
855 (
856 BED_FIELD_NAMES[index].to_string(),
857 BED_FIELD_STANDARD_TYPES[index].to_string(),
858 )
859 } else {
860 (format!("field{}", index + 1), "string".to_string())
861 }
862 })
863 .collect()
864}
865
866pub fn convert_to_bigbed(
871 input: &Path,
872 output: &Path,
873 mut options: BbiWriterOptions,
874 progress: Option<ProgressFn>,
875 cancel: Option<CancelFlag>,
876) -> Result<ConvertResult> {
877 options.kind = BbiKind::BigBed;
878 let mut reader = LineReader::open(input)?;
879 let tracker = ProgressTracker::with_callback(reader.size, progress);
880
881 let mut writer: Option<BbiWriter> = None;
886 let mut declared_fields = std::mem::take(&mut options.fields);
887 let mut col_count = 0usize;
888 let mut line_count = 0u64;
889 let mut reported = 0u64;
890 let mut values: IndexMap<String, String> = IndexMap::new();
891
892 let mut line = String::new();
893 let result = (|| -> Result<()> {
894 while reader.read_line_into(&mut line)? {
895 line_count += 1;
896 if line_count % PROGRESS_INTERVAL == 0 {
897 tracker.add(reader.consumed - reported);
898 reported = reader.consumed;
899 if cancel.as_ref().is_some_and(CancelFlag::is_cancelled) {
903 return Err(Error::invalid("conversion cancelled"));
904 }
905 }
906 if is_skipped_line(&line) {
907 continue;
908 }
909 let mut fields = split_tabs(&line);
910 trim_trailing_tab(
911 &mut fields,
912 if writer.is_none() {
913 (!declared_fields.is_empty()).then(|| declared_fields.len())
914 } else {
915 Some(col_count)
916 },
917 );
918 if fields.len() < 3 {
919 return Err(reader.fail(format!(
920 "bed record has {} tab-separated columns, and needs at least 3 \
921 (chrom, chromStart, chromEnd)",
922 fields.len()
923 )));
924 }
925
926 if writer.is_none() {
927 col_count = fields.len();
928 if declared_fields.is_empty() {
929 declared_fields = default_bed_fields(col_count);
930 } else if declared_fields.len() != col_count {
931 return Err(reader.fail(format!(
932 "fields declares {} columns and the first record has {col_count}",
933 declared_fields.len()
934 )));
935 }
936 let mut opened = BbiWriterOptions {
937 fields: declared_fields.clone(),
938 ..clone_options(&options)
939 };
940 opened.kind = BbiKind::BigBed;
941 writer = Some(
942 BbiWriter::create(&output.to_string_lossy(), opened)
943 .map_err(|e| reader.fail(e))?,
944 );
945 for name in declared_fields.keys().skip(3) {
946 values.insert(name.clone(), String::new());
947 }
948 } else if fields.len() != col_count {
949 return Err(reader.fail(format!(
950 "bed record has {} columns and the first one had {col_count}; a bigbed \
951 stores one shape of record",
952 fields.len()
953 )));
954 }
955
956 let start = parse_i64(fields[1], "chromStart").map_err(|e| reader.fail(e))?;
957 let end = parse_i64(fields[2], "chromEnd").map_err(|e| reader.fail(e))?;
958 for (index, slot) in values.values_mut().enumerate() {
962 slot.clear();
963 slot.push_str(fields[index + 3]);
964 }
965 writer
966 .as_mut()
967 .expect("opened above")
968 .write_entry(fields[0], start, end, &values)
969 .map_err(|e| reader.guard(e))?;
970 }
971 Ok(())
972 })();
973
974 if let Err(error) = result {
975 if let Some(writer) = &mut writer {
976 writer.abandon();
977 }
978 return Err(error);
979 }
980
981 let mut writer = match writer {
984 Some(writer) => writer,
985 None => {
986 if declared_fields.is_empty() {
987 declared_fields = default_bed_fields(3);
988 }
989 let mut opened = BbiWriterOptions {
990 fields: declared_fields,
991 ..clone_options(&options)
992 };
993 opened.kind = BbiKind::BigBed;
994 BbiWriter::create(&output.to_string_lossy(), opened)?
995 }
996 };
997 writer.close()?;
998 tracker.done_report();
999
1000 Ok(ConvertResult {
1001 format: TextFormat::Bed,
1002 line_count,
1003 item_count: writer.entry_count(),
1004 skipped_count: writer.skipped_count(),
1005 clipped_count: writer.clipped_count(),
1006 chr_sizes: writer.chr_sizes(),
1007 })
1008}
1009
1010fn clone_options(options: &BbiWriterOptions) -> BbiWriterOptions {
1014 BbiWriterOptions {
1015 kind: options.kind,
1016 chr_sizes: options.chr_sizes.clone(),
1017 fields: options.fields.clone(),
1018 items_per_slot: options.items_per_slot,
1019 block_size: options.block_size,
1020 compression_level: options.compression_level,
1021 parallel: options.parallel,
1022 section_policy: options.section_policy,
1023 cost_model: options.cost_model,
1024 }
1025}