diff --git a/Cargo.toml b/Cargo.toml index 6d86eab..16c4325 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ yewdux = "0.9" wasm-bindgen = "0.2" web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement", "HtmlDialogElement", "Blob", "Url", "Window", - "HtmlAnchorElement", "ReadableStream", + "HtmlAnchorElement", "ReadableStream", "HtmlSelectElement", "HtmlOptionElement", "HtmlButtonElement", "FileReader"] } js-sys = "0.3" log = "0.4" diff --git a/src/components/main_window.rs b/src/components/main_window.rs index 82a8560..9d6cab8 100644 --- a/src/components/main_window.rs +++ b/src/components/main_window.rs @@ -1,18 +1,27 @@ #![allow(non_snake_case)] +use std::collections::HashSet; + use js_sys::Array; +use lazy_static::lazy_static; +use regex::Regex; use wasm_bindgen::{prelude::*, JsCast, JsValue}; -use web_sys::{Blob, HtmlAnchorElement, HtmlDialogElement, HtmlFormElement, HtmlInputElement, Url}; +use web_sys::{ + Blob, HtmlAnchorElement, HtmlButtonElement, HtmlDialogElement, HtmlFormElement, + HtmlInputElement, HtmlOptionElement, HtmlSelectElement, Url, +}; use yew::prelude::*; use yewdux::prelude::*; use super::new_plate_dialog::NewPlateDialog; use super::plates::plate_container::PlateContainer; use super::states::{CurrentTransfer, MainState}; -use super::transfer_menu::TransferMenu; +use super::transfer_menu::{letters_to_num, RegionDisplay, TransferMenu}; use super::tree::Tree; use crate::data::csv::state_to_csv; use crate::data::plate_instances::PlateInstance; +use crate::data::transfer::Transfer; +use crate::data::transfer_region::{Region, TransferRegion}; #[function_component] pub fn MainWindow() -> Html { @@ -100,6 +109,7 @@ pub fn MainWindow() -> Html { }; let import_json_button_callback = { + let main_dispatch = main_dispatch.clone(); Callback::from(move |_| { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); @@ -177,6 +187,273 @@ pub fn MainWindow() -> Html { }) }; + let import_transfer_csv_callback = { + Callback::from(move |_| { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + let modal = document + .create_element("dialog") + .unwrap() + .dyn_into::() + .unwrap(); + modal.set_text_content(Some("Import File:")); + let onclose_callback = { + let modal = modal.clone(); + Closure::::new(move |_: Event| { + modal.remove(); + }) + }; + modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref())); + onclose_callback.forget(); + + let form = document + .create_element("form") + .unwrap() + .dyn_into::() + .unwrap(); + let input = document + .create_element("input") + .unwrap() + .dyn_into::() + .unwrap(); + input.set_type("file"); + input.set_accept(".csv"); + form.append_child(&input).unwrap(); + + let input_callback = { + let main_dispatch = main_dispatch.clone(); + let modal = modal.clone(); + Closure::::new(move |e: Event| { + if let Some(input) = e.current_target() { + let input = input + .dyn_into::() + .expect("We know this is an input."); + if let Some(files) = input.files() { + if let Some(file) = files.get(0) { + let fr = web_sys::FileReader::new().unwrap(); + fr.read_as_text(&file).unwrap(); + let fr1 = fr.clone(); // Clone to avoid outliving closure + let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut + // trait + let modal = modal.clone(); + let onload = Closure::::new(move |_: Event| { + if let Some(value) = + &fr1.result().ok().and_then(|v| v.as_string()) + { + let mut rdr = csv::Reader::from_reader(value.as_bytes()); + let mut records = Vec::new(); + for record in + rdr.deserialize::() + { + match record { + Ok(r) => { + //log::debug!("{:?}", r); + records.push(r); + } + Err(e) => { + log::debug!("{:?}", e); + } + } + } + + let mut sources: HashSet = HashSet::new(); + let mut destinations: HashSet = HashSet::new(); + for record in records.iter() { + sources.insert(record.source_plate.clone()); + destinations.insert(record.destination_plate.clone()); + } + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let form = document + .create_element("form") + .unwrap() + .dyn_into::() + .unwrap(); + let from_source = document + .create_element("select") + .unwrap() + .dyn_into::() + .unwrap(); + for source in sources { + let option = document + .create_element("option") + .unwrap() + .dyn_into::() + .unwrap(); + option.set_value(&source); + option.set_text(&source); + from_source.append_child(&option).unwrap(); + } + let to_source = document + .create_element("select") + .unwrap() + .dyn_into::() + .unwrap(); + for source in &main_dispatch.get().source_plates { + let option = document + .create_element("option") + .unwrap() + .dyn_into::() + .unwrap(); + option.set_value(&source.name); + option.set_text(&source.name); + to_source.append_child(&option).unwrap(); + } + let from_dest = document + .create_element("select") + .unwrap() + .dyn_into::() + .unwrap(); + for dest in destinations { + let option = document + .create_element("option") + .unwrap() + .dyn_into::() + .unwrap(); + option.set_value(&dest); + option.set_text(&dest); + from_dest.append_child(&option).unwrap(); + } + let to_dest = document + .create_element("select") + .unwrap() + .dyn_into::() + .unwrap(); + for dest in &main_dispatch.get().destination_plates { + let option = document + .create_element("option") + .unwrap() + .dyn_into::() + .unwrap(); + option.set_value(&dest.name); + option.set_text(&dest.name); + to_dest.append_child(&option).unwrap(); + } + let submit = document + .create_element("button") + .unwrap() + .dyn_into::() + .unwrap(); + submit.set_value("Submit"); + let submit_callback = { + let main_dispatch = main_dispatch.clone(); + let from_source = from_source.clone(); + let to_source = to_source.clone(); + let from_dest = from_dest.clone(); + let to_dest = to_dest.clone(); + Closure::::new(move |_: Event| { + let from_source = from_source.value(); + let to_source = to_source.value(); + let from_dest = from_dest.value(); + let to_dest = to_dest.value(); + + lazy_static! { + static ref REGEX: Regex = + Regex::new(r"([A-Z]+)(\d+)").unwrap(); + } + let records: Vec<((u8, u8), (u8, u8))> = records + .iter() + .filter(|record| { + record.source_plate == from_source + }) + .filter(|record| { + record.destination_plate == from_dest + }) + .map(|record| { + let c1 = REGEX + .captures(&record.source_well) + .unwrap(); + let c2 = REGEX + .captures(&record.destination_well) + .unwrap(); + log::debug!("{} {}", &record.source_well, &record.destination_well); + log::debug!("{},{} {},{}", &c1[1], &c1[2], &c2[1], &c2[2]); + + ( + ( + letters_to_num(&c1[1]).unwrap(), + c1[2].parse::().unwrap(), + ), + ( + letters_to_num(&c2[1]).unwrap(), + c2[2].parse::().unwrap(), + ), + ) + }) + .collect(); + + let spi = main_dispatch + .get() + .source_plates + .iter() + .find(|src| src.name == to_source) + .unwrap() + .clone(); + let dpi = main_dispatch + .get() + .destination_plates + .iter() + .find(|dest| dest.name == to_dest) + .unwrap() + .clone(); + + let custom_region = Region::new_custom(&records); + let transfer_region = TransferRegion { + source_region: custom_region.clone(), + dest_region: custom_region, + interleave_source: (1, 1), + interleave_dest: (1, 1), + source_plate: spi.plate, + dest_plate: dpi.plate, + }; + + let transfer = Transfer::new( + spi, + dpi, + transfer_region, + "Custom Transfer".to_string(), + ); + main_dispatch.reduce_mut(|state| { + state.transfers.push(transfer); + state.selected_transfer = state + .transfers + .last() + .expect("An element should have just been added") + .get_uuid(); + }); + }) + }; + submit.set_onclick(Some( + submit_callback.as_ref().unchecked_ref(), + )); + submit_callback.forget(); + + form.append_child(&from_source).unwrap(); + form.append_child(&to_source).unwrap(); + form.append_child(&from_dest).unwrap(); + form.append_child(&to_dest).unwrap(); + modal.append_child(&submit).unwrap(); + modal.append_child(&form).unwrap(); + } + }); + fr.set_onload(Some(onload.as_ref().unchecked_ref())); + onload.forget(); // Magic (don't touch) + } + } + } + }) + }; + input.set_onchange(Some(input_callback.as_ref().unchecked_ref())); + input_callback.forget(); // Magic straight from the docs, don't touch :( + + modal.append_child(&form).unwrap(); + body.append_child(&modal).unwrap(); + modal.show_modal().unwrap(); + }) + }; + html! { <>
@@ -190,7 +467,13 @@ pub fn MainWindow() -> Html {
- +
diff --git a/src/components/transfer_menu.rs b/src/components/transfer_menu.rs index 4da66ad..fa9f343 100644 --- a/src/components/transfer_menu.rs +++ b/src/components/transfer_menu.rs @@ -350,6 +350,37 @@ impl TryFrom for RegionDisplay { } } } +impl TryFrom<&str> for RegionDisplay { + type Error = &'static str; + + fn try_from(value: &str) -> Result { + lazy_static! { + static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap(); + } + if let Some(captures) = REGION_REGEX.captures(&value) { + if captures.len() != 5 { + return Err("Not enough capture groups"); + } + let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?; + let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?; + let row_start: u8 = captures[2] + .parse::() + .or(Err("Row start failed to parse"))?; + let row_end: u8 = captures[4] + .parse::() + .or(Err("Row end failed to parse"))?; + Ok(RegionDisplay { + text: value.to_string(), + col_start, + row_start, + col_end, + row_end, + }) + } else { + Err("Regex match failed") + } + } +} impl From<&Region> for RegionDisplay { fn from(value: &Region) -> Self { match *value { @@ -392,9 +423,10 @@ impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay { }) } } -fn letters_to_num(letters: &str) -> Option { +pub fn letters_to_num(letters: &str) -> Option { let mut num: u8 = 0; - for (i, letter) in letters.chars().rev().enumerate() { + for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() { + log::debug!("{}, {}", i, letter); let n = letter as u8; if !(65..=90).contains(&n) { return None; diff --git a/src/data/csv.rs b/src/data/csv.rs index 9306883..ec3e7b3 100644 --- a/src/data/csv.rs +++ b/src/data/csv.rs @@ -2,23 +2,23 @@ use crate::components::states::MainState; use crate::components::transfer_menu::num_to_letters; use crate::data::transfer::Transfer; -use serde::Serialize; +use serde::{Serialize, Deserialize}; use std::error::Error; -#[derive(Serialize, Debug)] -struct TransferRecord { - #[serde(rename = "Source Plate Barcode")] - source_plate: String, +#[derive(Serialize, Deserialize, Debug)] +pub struct TransferRecord { + #[serde(rename = "Source Plate")] + pub source_plate: String, #[serde(rename = "Source Well")] - source_well: String, - #[serde(rename = "Destination Plate Barcode")] - destination_plate: String, + pub source_well: String, + #[serde(rename = "Dest Plate")] + pub destination_plate: String, #[serde(rename = "Destination Well")] - destination_well: String, - #[serde(rename = "Volume")] - volume: f32, + pub destination_well: String, + #[serde(rename = "Transfer Volume")] + pub volume: f32, #[serde(rename = "Concentration")] - concentration: Option, + pub concentration: Option, } pub fn state_to_csv(state: &MainState) -> Result> { diff --git a/src/data/transfer_region.rs b/src/data/transfer_region.rs index b68ca3a..35ee1dd 100644 --- a/src/data/transfer_region.rs +++ b/src/data/transfer_region.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::components::transfer_menu::RegionDisplay; + use super::plate::Plate; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] @@ -30,6 +32,22 @@ impl TryFrom for ((u8, u8), (u8, u8)) { } } } +impl Region { + pub fn new_custom(transfers: &Vec<((u8, u8), (u8, u8))>) -> Self { + let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len()); + let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len()); + + for transfer in transfers { + src_pts.push(transfer.0); + dest_pts.push(transfer.1); + } + + Region::Custom(CustomRegion { + src: src_pts, + dest: dest_pts, + }) + } +} #[derive(PartialEq, Eq, Clone, Serialize, Deserialize, Debug)] pub struct TransferRegion { @@ -281,7 +299,7 @@ impl TransferRegion { // log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1); return Err("Source region is out-of-bounds! (Too wide)"); } - }, + } Region::Custom(_) => return Ok(()), }