use crate::util::dna::reverse_complement;
use anyhow::{ensure, Context, Result};
use bio::alignment::sparse::{hash_kmers, HashMapFx};
use fgoxide::io::Io;
use itertools::{self, Itertools};
use seq_io::fasta::{Reader as FastaReader, Record as FastaRecord};
use std::path::PathBuf;
use std::{collections::HashMap, io::BufRead};
const BUFFER_SIZE: usize = 1024 * 1024;
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct TargetSeq {
pub name: String,
pub fwd: Vec<u8>,
pub revcomp: Vec<u8>,
pub circular: bool,
}
pub struct TargetHash<'a> {
pub name: String,
pub fwd_hash: HashMapFx<&'a [u8], Vec<u32>>,
pub revcomp_hash: HashMapFx<&'a [u8], Vec<u32>>,
}
impl TargetSeq {
pub fn new(name: &str, seq: &Vec<u8>, circular: bool) -> Self {
Self {
name: name.to_string(),
fwd: seq.clone(),
revcomp: reverse_complement(seq),
circular,
}
}
pub fn len(&self) -> usize {
self.fwd.len()
}
pub fn is_empty(&self) -> bool {
self.fwd.len() == 0
}
pub fn build_target_hash(&self, k: usize) -> TargetHash<'_> {
TargetHash {
name: self.name.clone(),
fwd_hash: hash_kmers(&self.fwd, k),
revcomp_hash: hash_kmers(&self.revcomp, k),
}
}
}
fn header_to_name(header: &[u8]) -> Result<String> {
let header: std::borrow::Cow<str> = String::from_utf8_lossy(header);
header
.split_whitespace()
.next()
.map(std::string::ToString::to_string)
.context("empty read name")
}
pub fn from_fasta(file: &PathBuf, circular: bool) -> Result<Vec<TargetSeq>> {
let fg_io: Io = Io::new(5, BUFFER_SIZE);
let dict = file.as_path().with_extension(".dict");
let circular_contigs = if dict.exists() {
let mut circular_contigs = HashMap::new();
for line in fg_io.read_lines(&dict)? {
if !line.starts_with("@SQ") {
continue;
}
let fields = line.split_ascii_whitespace().collect_vec();
let contig_is_circular = fields
.iter()
.filter(|field| field.starts_with("TP"))
.filter_map(|field| field.split_terminator(':').next_back())
.any(|field| field == "circular");
let contig_name = fields
.iter()
.filter(|field| field.starts_with("SN"))
.find_map(|field| field.split_terminator(':').next_back())
.unwrap();
circular_contigs.insert(contig_name.to_owned(), contig_is_circular);
}
circular_contigs
} else {
HashMap::new()
};
let source: FastaReader<Box<dyn BufRead + Send>> =
FastaReader::with_capacity(fg_io.new_reader(file)?, BUFFER_SIZE);
let sequences = source
.into_records()
.map(|r| r.unwrap_or_else(|_| panic!("Error reading FASTA")))
.collect_vec();
ensure!(!sequences.is_empty(), "Found no sequences in the FASTA");
sequences
.iter()
.map(|record| {
let sequence = record
.seq()
.iter()
.map(u8::to_ascii_uppercase)
.collect_vec();
let name = header_to_name(record.head())?;
let contig_is_circular = circular_contigs
.get(&name)
.map_or(circular, |circular| *circular);
Ok(TargetSeq::new(&name, &sequence, contig_is_circular))
})
.collect()
}