use bio_files::SeqRecordAb1;
use copypasta::{ClipboardContext, ClipboardProvider};
use eframe::{
egui::{
Align2, Color32, FontFamily, FontId, Frame, Pos2, Rect, RichText, Sense, Shape, Slider,
Stroke, Ui, pos2, vec2,
},
emath::RectTransform,
epaint::PathShape,
};
use na_seq::{Nucleotide, seq_to_str_lower};
use crate::{
feature_db_load::find_features,
file_io::GenericData,
gui::{BACKGROUND_COLOR, COL_SPACING, ROW_SPACING},
misc_types::Metadata,
state::State,
util::merge_feature_sets,
};
const NT_COLOR: Color32 = Color32::from_rgb(180, 220, 220);
const NT_WIDTH: f32 = 8.; const STROKE_WIDTH_PEAK: f32 = 1.;
const PEAK_WIDTH_DIV2: f32 = 2.;
const PEAK_MAX_HEIGHT: f32 = 120.;
const COLOR_A: Color32 = Color32::from_rgb(20, 220, 20);
const COLOR_C: Color32 = Color32::from_rgb(130, 130, 255);
const COLOR_T: Color32 = Color32::from_rgb(255, 100, 100);
const COLOR_G: Color32 = Color32::from_rgb(200, 200, 200);
fn nt_color_map(nt: Nucleotide) -> Color32 {
match nt {
Nucleotide::A => COLOR_A,
Nucleotide::C => COLOR_C,
Nucleotide::T => COLOR_T,
Nucleotide::G => COLOR_G,
}
}
fn index_to_posit(i: usize, num_nts: usize, ui: &Ui) -> f32 {
i as f32 * NT_WIDTH
}
fn plot(data: &SeqRecordAb1, to_screen: &RectTransform, start_i: usize, ui: &mut Ui) -> Vec<Shape> {
let mut result = Vec::new();
let nt_y = 160.;
let plot_y = nt_y - 20.;
let data_scaler = {
let mut max_peak = 0;
for i in 0..data.data_ch1.len() {
if i > 40 {
continue;
}
let ch1 = data.data_ch1[i];
let ch2 = data.data_ch2[i];
let ch3 = data.data_ch3[i];
let ch4 = data.data_ch4[i];
for ch in [ch1, ch2, ch3, ch4] {
if ch > max_peak {
max_peak = ch;
}
}
}
PEAK_MAX_HEIGHT / max_peak as f32
};
let width = ui.available_width();
let num_nts_disp = width / NT_WIDTH;
for i_pos in 0..num_nts_disp as usize {
let i = start_i + i_pos;
if data.sequence.is_empty() || i > data.sequence.len() - 1 {
break;
}
let nt = data.sequence[i];
let x_pos = index_to_posit(i_pos, data.sequence.len(), ui);
if x_pos > ui.available_width() - 15. {
continue; }
let nt_color = nt_color_map(nt);
result.push(ui.ctx().fonts(|fonts| {
Shape::text(
fonts,
to_screen * pos2(x_pos, nt_y),
Align2::CENTER_CENTER,
&nt.to_str_lower(),
FontId::new(12., FontFamily::Monospace),
nt_color,
)
}));
}
for i_pos in 0..num_nts_disp as usize * 4 {
let i = start_i * 4 + i_pos;
if data.data_ch1.is_empty() || i > data.data_ch1.len() - 1 {
break;
}
let x_pos = index_to_posit(i_pos, data.sequence.len(), ui) / 4.;
if x_pos > ui.available_width() - 15. {
continue; }
let ch1 = data.data_ch1[i];
let ch2 = data.data_ch2[i];
let ch3 = data.data_ch3[i];
let ch4 = data.data_ch4[i];
for (ch, color) in [
(ch1, COLOR_G),
(ch2, COLOR_A),
(ch3, COLOR_T),
(ch4, COLOR_C),
] {
let stroke = Stroke::new(STROKE_WIDTH_PEAK, color);
let base_pos = pos2(x_pos, plot_y);
let top_left = base_pos + vec2(-PEAK_WIDTH_DIV2, ch as f32 * -data_scaler);
let top_right = base_pos + vec2(PEAK_WIDTH_DIV2, ch as f32 * -data_scaler);
let bottom_left = base_pos + vec2(-PEAK_WIDTH_DIV2, 0.);
let bottom_right = base_pos + vec2(PEAK_WIDTH_DIV2, 0.);
result.push(ui.ctx().fonts(|fonts| {
Shape::Path(PathShape::closed_line(
vec![
to_screen * top_left,
to_screen * bottom_left,
to_screen * bottom_right,
to_screen * top_right,
],
stroke,
))
}));
}
}
result
}
pub fn ab1_page(state: &mut State, ui: &mut Ui) {
ui.horizontal(|ui| {
let data = &state.ab1_data[state.active];
ui.heading("AB1 sequencing view");
ui.add_space(COL_SPACING * 2.);
if ui.button(RichText::new("🗐 Copy sequence")).on_hover_text("Copy this sequence to the clipboard.").clicked() {
let mut ctx = ClipboardContext::new().unwrap();
ctx.set_contents(seq_to_str_lower(&data.sequence)).unwrap();
}
ui.add_space(COL_SPACING);
if ui
.button("➕ Create data (e.g. Genbank, PCAD etc) as a new tab.")
.on_hover_text("Create a new non-AB1 data set\
that may include features, primers, etc. May be edited, and saved to Genbank, PCAD, or SnapGene formats.")
.clicked()
{
let plasmid_name = match &state.tabs_open[state.active].path {
Some(p) => p.file_name().unwrap().to_str().unwrap_or_default(),
None => "Plasmid from AB1", }.to_owned().replace(".ab1", "");
let generic = GenericData {
seq: data.sequence.clone(),
metadata: Metadata {
plasmid_name,
..Default::default()
},
..Default::default()
};
state.generic.push(generic);
state.portions.push(Default::default());
state.volatile.push(Default::default());
state.tabs_open.push(Default::default());
state.ab1_data.push(Default::default());
state.active = state.generic.len() - 1;
let features = find_features(&state.get_seq());
merge_feature_sets(&mut state.generic[state.active].features, &features)
}
});
ui.add_space(ROW_SPACING / 2.);
let data = &state.ab1_data[state.active];
let width = ui.available_width();
let num_nts_disp = width / NT_WIDTH;
ui.spacing_mut().slider_width = width - 60.;
let slider_max = {
let v = data.sequence.len() as isize - num_nts_disp as isize + 1;
if v > 0 { v as usize } else { 0 }
};
ui.add(Slider::new(&mut state.ui.ab1_start_i, 0..=slider_max));
ui.add_space(ROW_SPACING / 2.);
let mut shapes = Vec::new();
Frame::canvas(ui.style())
.fill(BACKGROUND_COLOR)
.show(ui, |ui| {
let data = &state.ab1_data[state.active];
let (response, _painter) = {
let desired_size = vec2(ui.available_width(), ui.available_height());
ui.allocate_painter(desired_size, Sense::click())
};
let to_screen = RectTransform::from_to(
Rect::from_min_size(Pos2::ZERO, response.rect.size()),
response.rect,
);
let rect_size = response.rect.size();
shapes.append(&mut plot(data, &to_screen, state.ui.ab1_start_i, ui));
ui.painter().extend(shapes);
});
}