Compare commits

...

5 Commits

7 changed files with 394 additions and 26 deletions

View File

@ -10,9 +10,9 @@ yew = { version = "0.20.0", features = ["csr"] }
yewdux = "0.9"
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement",
"HtmlDialogElement", "Blob", "Url", "Window",
"HtmlAnchorElement", "ReadableStream",
"FileReader"] }
"HtmlDialogElement", "Blob", "Url", "Window",
"HtmlAnchorElement", "ReadableStream", "HtmlSelectElement", "HtmlOptionElement", "HtmlButtonElement",
"FileReader"] }
js-sys = "0.3"
log = "0.4"
wasm-logger = "0.2"

View File

@ -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::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::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::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.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::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.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::<dyn FnMut(_)>::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::<crate::data::csv::TransferRecord>()
{
match record {
Ok(r) => {
//log::debug!("{:?}", r);
records.push(r);
}
Err(e) => {
log::debug!("{:?}", e);
}
}
}
let mut sources: HashSet<String> = HashSet::new();
let mut destinations: HashSet<String> = 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::<HtmlFormElement>()
.unwrap();
let from_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in sources {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.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::<HtmlSelectElement>()
.unwrap();
for source in &main_dispatch.get().source_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.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::<HtmlSelectElement>()
.unwrap();
for dest in destinations {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.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::<HtmlSelectElement>()
.unwrap();
for dest in &main_dispatch.get().destination_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.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::<HtmlButtonElement>()
.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::<dyn FnMut(_)>::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::<u8>().unwrap(),
),
(
letters_to_num(&c2[1]).unwrap(),
c2[2].parse::<u8>().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! {
<>
<div class="upper_menu">
@ -190,7 +467,13 @@ pub fn MainWindow() -> Html {
<button onclick={export_json_button_callback}>{"Export as JSON"}</button>
</div>
</div>
<button onclick={import_json_button_callback}>{"Import"}</button>
<div class="dropdown-sub">
<button>{"Import"}</button>
<div>
<button onclick={import_json_button_callback}>{"Import from JSON"}</button>
<button onclick={import_transfer_csv_callback}>{"Import Transfer from CSV"}</button>
</div>
</div>
</div>
</div>
<div class="main_container">

View File

@ -34,6 +34,7 @@ pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
let (pt1, pt2) = match ct_state.transfer.transfer_region.dest_region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0,0), (0,0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));

View File

@ -34,6 +34,7 @@ pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let (pt1, pt2) = match ct_state.transfer.transfer_region.source_region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0,0), (0,0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));

View File

@ -39,6 +39,9 @@ pub fn TransferMenu() -> Html {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
if matches!(ct_dispatch.get().transfer.transfer_region.source_region, Region::Custom(_)) {
return; // Do nothing here!
}
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
@ -192,7 +195,7 @@ pub fn TransferMenu() -> Html {
let new_transfer = Transfer::new(
spi.clone(),
dpi.clone(),
ct_state.transfer.transfer_region,
ct_state.transfer.transfer_region.clone(),
ct_state.transfer.name.clone(),
);
main_dispatch.reduce_mut(|state| {
@ -250,6 +253,8 @@ pub fn TransferMenu() -> Html {
onchange={on_name_change}
value={ct_state.transfer.name.clone()}/>
</div>
// Anything below here is not rendered when a Custom transfer is selected
if !matches!(&ct_state.transfer.transfer_region.source_region, Region::Custom(_)) {
<div>
<label for="src_region"><h3>{"Source Region:"}</h3></label>
<input type="text" name="src_region"
@ -291,6 +296,7 @@ pub fn TransferMenu() -> Html {
onchange={on_volume_change}
value={ct_state.transfer.volume.to_string()}/>
</div>
}
<div id="controls">
<input type="button" name="new_transfer" onclick={new_transfer_button_callback}
value={"New"} />
@ -344,6 +350,37 @@ impl TryFrom<String> for RegionDisplay {
}
}
}
impl TryFrom<&str> for RegionDisplay {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
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::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.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 {
@ -353,6 +390,7 @@ impl From<&Region> for RegionDisplay {
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1))
.ok()
.unwrap(),
Region::Custom(_) => RegionDisplay { text: "CUSTOM".to_string(), col_start: 0, row_start: 0, col_end: 0, row_end: 0 }
}
}
}
@ -385,9 +423,10 @@ impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay {
})
}
}
fn letters_to_num(letters: &str) -> Option<u8> {
pub fn letters_to_num(letters: &str) -> Option<u8> {
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;

View File

@ -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<f32>,
pub concentration: Option<f32>,
}
pub fn state_to_csv(state: &MainState) -> Result<String, Box<dyn Error>> {

View File

@ -1,11 +1,20 @@
use serde::{Deserialize, Serialize};
use crate::components::transfer_menu::RegionDisplay;
use super::plate::Plate;
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct CustomRegion {
src: Vec<(u8, u8)>,
dest: Vec<(u8, u8)>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region {
Rect((u8, u8), (u8, u8)),
Point((u8, u8)),
Custom(CustomRegion),
}
impl Default for Region {
fn default() -> Self {
@ -23,8 +32,24 @@ impl TryFrom<Region> 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());
#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Debug)]
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 {
pub source_plate: Plate,
pub source_region: Region, // Even if it is just a point, we don't want corners.
@ -49,7 +74,7 @@ impl Default for TransferRegion {
impl TransferRegion {
pub fn get_source_wells(&self) -> Vec<(u8, u8)> {
match self.source_region {
match &self.source_region {
Region::Rect(c1, c2) => {
let mut wells = Vec::<(u8, u8)>::new();
let (ul, br) = standardize_rectangle(&c1, &c2);
@ -71,7 +96,8 @@ impl TransferRegion {
}
wells
}
Region::Point(p) => vec![p],
Region::Point(p) => vec![*p],
Region::Custom(c) => c.src.clone(),
}
}
@ -112,6 +138,7 @@ impl TransferRegion {
let source_corners: ((u8, u8), (u8, u8)) = match self.source_region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0, 0), (0, 0)),
};
let (source_ul, _) = standardize_rectangle(&source_corners.0, &source_corners.1);
// This map is not necessarily injective or surjective,
@ -120,7 +147,7 @@ impl TransferRegion {
// and simple then we *will* have injectivity.
// Non-replicate transfers:
match self.dest_region {
match &self.dest_region {
Region::Point((x, y)) => {
Box::new(move |(i, j)| {
if source_wells.contains(&(i, j)) {
@ -224,6 +251,22 @@ impl TransferRegion {
}
})
}
Region::Custom(c) => Box::new(move |(i, j)| {
let src = c.src.clone();
let dest = c.dest.clone();
let points: Vec<(u8, u8)> = src
.iter()
.enumerate()
.filter(|(_index, (x, y))| *x == i && *y == j)
.map(|(index, _)| dest[index])
.collect();
if points.is_empty() {
None
} else {
Some(points)
}
}),
}
}
@ -257,6 +300,7 @@ impl TransferRegion {
return Err("Source region is out-of-bounds! (Too wide)");
}
}
Region::Custom(_) => return Ok(()),
}
if il_source.0 == 0 || il_dest.1 == 0 {