use crate::errors::{Error, ErrorKind};
use log::warn;
use rust_htslib::bam::HeaderView;
use std::ops::Not;
pub struct RawPileupRegion {
name: String,
start: i64,
end: i64,
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct GenomeInterval {
pub tid: i64,
pub name: String,
pub start: i64,
pub end: i64,
}
pub struct GenomeIntervalIterator<'a> {
cur_start: i64,
cur_end: i64,
chunk_size: i64,
interval: &'a GenomeInterval,
name: String,
exhausted: bool,
}
impl<'a> GenomeIntervalIterator<'a> {
pub fn new(interval: &'a GenomeInterval, chunk_size: i64) -> Self {
Self {
cur_start: interval.start,
cur_end: interval.start + chunk_size,
chunk_size,
interval,
name: interval.name.to_string(),
exhausted: false,
}
}
}
impl Iterator for GenomeIntervalIterator<'_> {
type Item = GenomeInterval;
fn next(&mut self) -> Option<Self::Item> {
let ret: Option<Self::Item>;
if self.exhausted {
return None;
};
if self.chunk_size > self.interval.end - self.interval.start {
self.exhausted = true;
return Some(self.interval.clone());
}
if self.cur_end < self.interval.end {
ret = Some(GenomeInterval {
tid: self.interval.tid,
name: self.name.to_string(),
start: self.cur_start,
end: self.cur_end,
});
} else if self.cur_start < self.interval.end {
ret = Some(GenomeInterval {
tid: self.interval.tid,
name: self.name.to_string(),
start: self.cur_start,
end: std::cmp::min(self.cur_end, self.interval.end),
});
} else {
self.exhausted = true;
ret = None
}
self.cur_start += self.chunk_size;
self.cur_end += self.chunk_size;
ret
}
}
impl GenomeInterval {
#[allow(dead_code)]
pub fn chunks(&self, chunk_size: i64) -> GenomeIntervalIterator<'_> {
GenomeIntervalIterator::new(self, chunk_size)
}
#[allow(dead_code)]
pub fn n_chunks(&self, n_chunks: i64) -> GenomeIntervalIterator<'_> {
if n_chunks == 1 {
GenomeIntervalIterator::new(self, i64::MAX)
} else {
GenomeIntervalIterator::new(self, (self.end - self.start) / n_chunks + 1)
}
}
pub fn is_empty(&self) -> bool {
self.end >= self.start
}
pub fn len(&self) -> i64 {
self.end - self.start
}
}
fn split_check_ends(s: &str, delim: char) -> Option<(Option<&str>, Option<&str>)> {
s.split_once(delim).map(|(pre, post)| {
(
pre.is_empty().not().then_some(pre),
post.is_empty().not().then_some(post),
)
})
}
fn parse_region_string(s: &str) -> Result<RawPileupRegion, Error> {
match split_check_ends(s, ':') {
None => Ok(RawPileupRegion {
name: s.to_string(),
start: 0,
end: i64::MAX,
}),
Some((None, _)) => Err(Error::from(ErrorKind::BadInputRegions(format!(
"Invalid region string {s}: ref name must come before ':'"
)))),
Some((Some(_), None)) => Err(Error::from(ErrorKind::BadInputRegions(format!(
"Invalid region string {s}: coordinates must come after ':'"
)))),
Some((Some(ref_str), Some(pos_str))) => match split_check_ends(pos_str, '-') {
None => {
let pos = pos_str.replace(",", "").parse::<i64>()?;
if pos < 0 {
return Err(Error::from(ErrorKind::BadInputRegions(format!(
"Cannot have negative pos {pos}: {s}"
))));
};
Ok(RawPileupRegion {
name: ref_str.to_string(),
start: pos - 1,
end: i64::MAX,
})
}
Some((None, _)) => Err(Error::from(ErrorKind::BadInputRegions(format!(
"Invalid region string {s}: must have start coordinate before '-'"
)))),
Some((Some(_), None)) => Err(Error::from(ErrorKind::BadInputRegions(format!(
"Invalid region string {s}: must have end coordinate after '-'"
)))),
Some((Some(start), Some(end))) => {
let mut start_pos = start.replace(",", "").parse::<i64>()?;
let end_pos = end.replace(",", "").parse::<i64>()?;
if start_pos < 0 {
return Err(Error::from(ErrorKind::BadInputRegions(format!(
"Invalid region string {s}: cannot have negative start pos: {start_pos}"
))));
};
if start_pos == 0 {
warn!("start pos is 0: input regions are 1-based. Setting start pos to 1");
start_pos = 1;
}
if end_pos < 0 {
return Err(Error::from(ErrorKind::BadInputRegions(format!(
"Invalid region string {s}: cannot have negative end pos: {end_pos}"
))));
};
if end_pos < start_pos {
return Err(Error::from(ErrorKind::BadInputRegions(format!(
"Invalid region string {s}: cannot have start end pos ({end_pos}) be smaller than start pos ({start_pos})"
))));
};
Ok(RawPileupRegion {
name: ref_str.to_string(),
start: start_pos - 1,
end: end_pos - 1,
})
}
},
}
}
pub fn create_region_queue(argstr: &[String], header: &HeaderView) -> Result<Vec<GenomeInterval>, Error> {
let mut rawregions = vec![];
for s in argstr {
rawregions.push(parse_region_string(s)?);
}
intervals_from_regions(header, rawregions)
}
pub fn intervals_from_header(header: &HeaderView) -> Result<Vec<GenomeInterval>, Error> {
let mut queue = Vec::new();
for tid in 0..header.target_count() {
let end = header
.target_len(tid)
.ok_or(Error::from(ErrorKind::AnomalousData(format!(
"Faulty BAM header: unable to find target len for ref ID {tid}"
))))? as i64;
let name = header.tid2name(tid);
let reg = GenomeInterval {
name: std::str::from_utf8(name)?.to_string(),
tid: i64::from(tid),
start: 0,
end,
};
queue.push(reg);
}
Ok(queue)
}
pub fn intervals_from_regions(
header: &HeaderView,
regions: Vec<RawPileupRegion>,
) -> Result<Vec<GenomeInterval>, Error> {
if regions.is_empty() {
return Err(Error::from(ErrorKind::AnomalousData(
"List of BAM regions is empty!".to_string(),
)));
}
let tnames: Vec<&str> = header
.target_names()
.into_iter()
.map(|s| std::str::from_utf8(s))
.collect::<Result<Vec<&str>, _>>()?;
let mut queue = Vec::new();
for rawreg in regions {
let mut found = false;
for (tid, canonname) in tnames.iter().enumerate() {
if rawreg.name == *canonname {
let canonlen = header
.target_len(tid as u32)
.ok_or(Error::from(ErrorKind::AnomalousData(format!(
"Faulty BAM header: unable to find target len for ref ID {tid}"
))))?;
let end = if rawreg.end > canonlen as i64 {
canonlen as i64
} else {
rawreg.end + 1
};
found = true;
queue.push(GenomeInterval {
name: rawreg.name.clone(),
tid: i64::try_from(tid)?,
start: rawreg.start,
end,
})
}
}
if !found {
return Err(Error::from(ErrorKind::AnomalousData(format!(
"Couldn't find region name {} in BAM header",
rawreg.name
))));
}
}
Ok(queue)
}