Compare commits

..

No commits in common. "e8511c01b0320ac5472d456cabeeeb33f3e0cbfa" and "7defd8ff08aed1552ee2754fbbeef421d58f6ba9" have entirely different histories.

35 changed files with 390 additions and 1000 deletions

289
Cargo.lock generated
View File

@ -26,21 +26,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anymap" name = "anymap"
version = "1.0.0-beta.2" version = "1.0.0-beta.2"
@ -91,12 +76,6 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"
@ -139,19 +118,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"serde",
"windows-targets",
]
[[package]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -162,12 +128,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]] [[package]]
name = "csv" name = "csv"
version = "1.3.0" version = "1.3.0"
@ -224,22 +184,6 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
"serde",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -523,24 +467,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.5" version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.11" version = "0.2.11"
@ -552,29 +484,6 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@ -587,7 +496,7 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfd6201e7c30ccb24773cac7efa6fec1e06189d414b7439ce756a481c8bfbf53" checksum = "cfd6201e7c30ccb24773cac7efa6fec1e06189d414b7439ce756a481c8bfbf53"
dependencies = [ dependencies = [
"indexmap 1.9.3", "indexmap",
] ]
[[package]] [[package]]
@ -597,19 +506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown 0.12.3", "hashbrown",
"serde",
]
[[package]]
name = "indexmap"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0"
dependencies = [
"equivalent",
"hashbrown 0.14.5",
"serde",
] ]
[[package]] [[package]]
@ -660,21 +557,6 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -751,7 +633,7 @@ dependencies = [
[[package]] [[package]]
name = "plate-tool-lib" name = "plate-tool-lib"
version = "0.5.0" version = "0.4.0"
dependencies = [ dependencies = [
"csv", "csv",
"getrandom", "getrandom",
@ -761,14 +643,12 @@ dependencies = [
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
"serde_with",
"serde_yaml",
"uuid", "uuid",
] ]
[[package]] [[package]]
name = "plate-tool-web" name = "plate-tool-web"
version = "0.5.0" version = "0.4.0"
dependencies = [ dependencies = [
"csv", "csv",
"getrandom", "getrandom",
@ -789,12 +669,6 @@ dependencies = [
"yewdux", "yewdux",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1007,49 +881,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_with"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
dependencies = [
"base64",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.3.0",
"serde",
"serde_derive",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.3.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@ -1107,37 +938,6 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.36.0" version = "1.36.0"
@ -1196,12 +996,6 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.7.0" version = "1.7.0"
@ -1351,79 +1145,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "yew" name = "yew"
version = "0.20.0" version = "0.20.0"
@ -1434,7 +1155,7 @@ dependencies = [
"futures", "futures",
"gloo", "gloo",
"implicit-clone", "implicit-clone",
"indexmap 1.9.3", "indexmap",
"js-sys", "js-sys",
"prokio", "prokio",
"rustversion", "rustversion",

View File

@ -1,45 +1,31 @@
# plate-tool # plate-tool
A web-based tool for creating and visualizing picklists for your favorite (possibly acoustic) liquid handler. A web-based tool for creating assays for your favorite (acoustic) liquid handler.
## Table of Contents ## Table of Contents
- [Usage](#usage) - [Usage](#Usage)
- [Plates](#plates) - [Installation](#Installation)
- [Adding plates](#adding-plates)
- [Modifying plates](#modifying-plates)
- [Transfers](#transfers)
- [Adding a transfer](#adding-a-transfer)
- [Modifying transfers](#modifying-transfers)
- [Importing and Exporting](#importing-and-Exporting)
- [Export as CSV](#export-as-csv)
- [Export as JSON](#export-as-json)
- [Import from JSON](#import-from-json)
- [Import Transfer from CSV](#import-transfer-from-csv)
- [Other Neat Features](#other-neat-features)
- [Installation](#installation)
## Usage ## Usage
### Plates ### Adding plates
#### Adding plates
When you open plate tool for the first time, When you open plate tool for the first time,
you'll be greeted by a message informing you that no plates are selected. you'll be greeted by a message informing you that no plates are selected.
To add a new plate, click the "Add" button for the corresponding plate type: To add a new plate, click the "New Plate" button:
Once you've added at least one source plate and one destination plate, Once you've added at least one source plate and one destination plate,
click one of each to select them. click one of each to select them.
The right-most pane will now display these plates. The right-most pane will now display these plates.
#### Modifying plates ### Modifying and deleting plates
Suppose you erroneously created a plate, or misspelled its name. Suppose you erroneously created a plate, or misspelled its name.
Double click on that plate in the list (top-left pane) and a new modal will open. Double click on that plate in the list (top-left pane) and a new modal will open.
Here you can rename a plate or delete it. Here you can rename a plate or delete it.
You may also change the format of a plate You may also change the format of a plate
(note that this will not delete any data if you accidentally switch to a smaller format). (note that this will not delete any data if you accidentally switch to a smaller format).
### Transfers ### Adding a transfer
#### Adding a transfer
Now that you have two plates selected, Now that you have two plates selected,
it's time to add a transfer. it's time to add a transfer.
We can see all of the properties of our transfer in the bottom-left pane. We can see all of the properties of our transfer in the bottom-left pane.
@ -60,7 +46,7 @@ depending on the transfer type and interleave settings.
When all of the settings are to your liking, click the "Save" button. When all of the settings are to your liking, click the "Save" button.
Note that it now appears in the "Transfers" section of the list pane. Note that it now appears in the "Transfers" section of the list pane.
#### Modifying transfers ### Modifying and deleting transfers
If you already saved a transfer and would like to change it, If you already saved a transfer and would like to change it,
click on its entry in the list. click on its entry in the list.
Now change the properties of the transfer as you did during initial creation. Now change the properties of the transfer as you did during initial creation.
@ -75,23 +61,10 @@ Exporting the transfers we have created to a CSV format is the primary (if not s
To do so, first note the "File" tab at the top-left of the screen (above the list pane). To do so, first note the "File" tab at the top-left of the screen (above the list pane).
Mouse over this tab, and a few more options will be revealed. Mouse over this tab, and a few more options will be revealed.
We want to export: mouse over export and select "Export as CSV". We want to export: mouse over export and select "Export as CSV".
You will be prompted by your browser to select a location for your file. You will be reminded that this is a one-way export (see JSON export/import below),
and then prompted by your browser to select a location for your file.
As of version `0.4.0`, it is possible to pick a CSV export format: #### Export as JSON (Saving Your Work)
Mouse over options, then export, then click "Change CSV export type".
In the dialog that opens, select your desired export type.
Currently, plate-tool supports:
- Normal
- This format can be imported by Cellario's cherrypick hook.
- Echo Client
- This format is useful if you want to run a picklist directly from the
Echo Client software.
This will export just the transfers between the currently selected plates;
I assume you'd be using this feature in a non-automation context
and know to load your plates into your Echo yourself.
#### Export as JSON
##### (Saving Your Work)
Currently, it is not possible to export to a format produced by other similar software. Currently, it is not possible to export to a format produced by other similar software.
However, you might reasonably want to save a copy of your work However, you might reasonably want to save a copy of your work
either as a backup or to share. either as a backup or to share.
@ -99,8 +72,7 @@ Mouse over the "File" tab, then "Export" as above, then alternatively select "Ex
Your browser will then prompt you to pick a suitable location to save your work as a file. Your browser will then prompt you to pick a suitable location to save your work as a file.
(See note 1 below) (See note 1 below)
#### Import from JSON #### Import from JSON (Recovering Your Work)
##### (Recovering Your Work)
If we want to import one such file, mouse over the "File" tab as before If we want to import one such file, mouse over the "File" tab as before
and select "Import", and finally click "Import from JSON". and select "Import", and finally click "Import from JSON".
This opens a modal where you are prompted to upload (see note 2) This opens a modal where you are prompted to upload (see note 2)
@ -122,8 +94,7 @@ You are welcome to verify (use your browser's developer tools, it should have a
that this application does not "phone home". that this application does not "phone home".
Your data is stored locally (unless you choose to export it and distribute it yourself). Your data is stored locally (unless you choose to export it and distribute it yourself).
#### Import Transfer from CSV #### Import Transfer from CSV (Using a picklist as a transfer)
##### (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool), If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer. you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV". To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
@ -139,11 +110,6 @@ there was likely an issue parsing your picklist.
Your browser's console may have guidance as to why parsing failed; Your browser's console may have guidance as to why parsing failed;
plate-tool was probably expecting a different name for a column than was in your file. plate-tool was probably expecting a different name for a column than was in your file.
_Note_: If you find a picklist that Cellario *can* import that plate-tool cannot,
please email me!
Odds are your picklist contains a weird edge case I've not considered,
and I would like to fix that!
### Other Neat Features ### Other Neat Features
#### Taking Pictures of Plates #### Taking Pictures of Plates
@ -153,7 +119,6 @@ plate-tool will do some magic to take a screenshot of your plate
and deposit it in your clipboard for you. and deposit it in your clipboard for you.
You can then paste this into PowerPoint, GIMP, or whereever else You can then paste this into PowerPoint, GIMP, or whereever else
you want a pretty picture of a plate. you want a pretty picture of a plate.
I hope this is helpful for arts and crafts.
_NOTE:_ I won't guarantee this feature will work in all contexts; _NOTE:_ I won't guarantee this feature will work in all contexts;
it relies on your browser thinking that you have plate-tool open it relies on your browser thinking that you have plate-tool open
@ -170,24 +135,13 @@ To disable the indicators, mouse over "Options" (top-left of screen), then "Styl
then click "Toggle transfer hashes". then click "Toggle transfer hashes".
To turn them back on, do the exact same thing. To turn them back on, do the exact same thing.
#### Transfer Volume Heatmap (Experimental)
This can be used to verify that all of the wells in a plate will have the same volume
transferred at a glance.
Wells will be colored based on the sum of all transfers using that plate.
To toggle this feature, mouse over "Options", then "Styles", then click "Toggle volume heatmap".
_NOTE_: The scale for the colors is spaced linearly; if you have a well that is being used
significantly more than some others, it may be difficult to see the difference between other wells
with more similar volumes.
If you have a use case that would benefit from a logarithmic scale here, please let me know.
## Installation ## Installation
Plate tool is hosted [here](https://ilia.moe/cool-stuff/plate-tool/) for your convenience. Plate tool is hosted [here](https://ilia.moe/cool-stuff/plate-tool/) for your convenience.
However, you're absolutely welcome to host your own instance (even locally). However, you're absolutely welcome to host your own instance (even locally).
Here's how: Here's how:
(_Note:_ If you run Windows you're absolutely fine to install rustup in Powershell, and the subsequent steps should be very similar but likely with different filepaths. I haven't personally validated this. ) (_Note:_ ~~If you run Windows you're probably best off doing the following in WSL2~~ You're absolutely fine to install rustup in Powershell, and the subsequent steps should be very similar but likely with different filepaths.)
1. Make sure you have a working Rust toolchain 1. Make sure you have a working Rust toolchain
1. Installing `rustup` is the easiest way to do this. See [their website](https://rustup.rs/), 1. Installing `rustup` is the easiest way to do this. See [their website](https://rustup.rs/),

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plate-tool-lib" name = "plate-tool-lib"
version = "0.5.0" version = "0.4.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -15,5 +15,3 @@ csv = "1.2"
getrandom = { version = "0.2", features = ["js"] } getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] } rand = { version = "0.8", features = ["small_rng"] }
lazy_static = "1.4" lazy_static = "1.4"
serde_with = { version = "3.9.0", features = ["json"] }
serde_yaml = "0.9.34"

View File

@ -2,10 +2,9 @@ use serde::Serialize;
use super::TransferRecord; use super::TransferRecord;
/// Format preferred by the Echo Client when using the Pick-List function. // Format preferred by the Echo Client when using the Pick-List function.
/// // Note that this does not export plate info!
/// Note that this does not export plate info! // Exports of this type should combine all transfers between the two actively selected plates.
/// Exports of this type should combine all transfers between the two actively selected plates.
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct EchoClientTransferRecord { pub struct EchoClientTransferRecord {
#[serde(rename = "SrcWell")] #[serde(rename = "SrcWell")]

View File

@ -1,17 +1,10 @@
//! Auto module for importing picklists without creating plates first
//!
//! Before the auto module, it was necessary for a user to create the plates
//! used in an imported transfer before import.
use super::{string_well_to_pt, TransferRecord, read_csv}; use super::{string_well_to_pt, TransferRecord, read_csv};
use crate::plate::{PlateFormat, PlateType}; use crate::plate::{PlateFormat, PlateType};
use crate::plate_instances::PlateInstance; use crate::plate_instances::PlateInstance;
use crate::transfer::Transfer; use crate::transfer::Transfer;
use crate::transfer_volume::{TransferVolume, VolumeMaps};
use crate::Well;
use crate::transfer_region::{CustomRegion, Region, TransferRegion}; use crate::transfer_region::{CustomRegion, Region, TransferRegion};
use std::collections::{HashSet, HashMap}; use std::collections::HashSet;
pub struct AutoOutput { pub struct AutoOutput {
pub sources: Vec<PlateInstance>, pub sources: Vec<PlateInstance>,
@ -19,7 +12,6 @@ pub struct AutoOutput {
pub transfers: Vec<Transfer>, pub transfers: Vec<Transfer>,
} }
/// Two lists of plates that are guaranteed to be unique (no duplicate plates)
struct UniquePlates { struct UniquePlates {
sources: Vec<PlateInstance>, sources: Vec<PlateInstance>,
destinations: Vec<PlateInstance>, destinations: Vec<PlateInstance>,
@ -32,12 +24,6 @@ const W384_ROW_MAX: u8 = 16;
const W1536_COL_MAX: u8 = 48; const W1536_COL_MAX: u8 = 48;
const W1536_ROW_MAX: u8 = 32; const W1536_ROW_MAX: u8 = 32;
/// Main function for the auto module
/// Takes a list of pre-deserialized transfer records and generates output
/// This output is 3 lists of:
/// 1. The source plates used in the picklist
/// 2. The destination plates used in the picklist
/// 3. All of the transfers between those plates
pub fn auto(records: &[TransferRecord]) -> AutoOutput { pub fn auto(records: &[TransferRecord]) -> AutoOutput {
let unique_plates = find_unique_plates(records); let unique_plates = find_unique_plates(records);
let transfers = get_transfer_for_all_pairs(records, &unique_plates); let transfers = get_transfer_for_all_pairs(records, &unique_plates);
@ -45,18 +31,11 @@ pub fn auto(records: &[TransferRecord]) -> AutoOutput {
AutoOutput { sources: unique_plates.sources, destinations: unique_plates.destinations, transfers } AutoOutput { sources: unique_plates.sources, destinations: unique_plates.destinations, transfers }
} }
/// Helper function that reads a CSV and immediately dumps the resulting transfer records
/// into the auto function defined in this module.
pub fn read_csv_auto(data: &str) -> AutoOutput { pub fn read_csv_auto(data: &str) -> AutoOutput {
let transfer_records = read_csv(data); let transfer_records = read_csv(data);
auto(&transfer_records) auto(&transfer_records)
} }
/// Looks through a list of transfer records and generates two lists
/// of all of the unique plates found in those transfers.
/// Worth noting that the Transfer struct requires two associated PlateInstances;
/// these PlateInstance structs are generated here---they are not present in a TransferRecord.
/// See notes on the UniquePlates struct
fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates { fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates {
let mut source_names: HashSet<&str> = HashSet::new(); let mut source_names: HashSet<&str> = HashSet::new();
let mut destination_names: HashSet<&str> = HashSet::new(); let mut destination_names: HashSet<&str> = HashSet::new();
@ -100,16 +79,10 @@ fn find_unique_plates(records: &[TransferRecord]) -> UniquePlates {
} }
} }
/// Tries to guess the format of a plate given the southeastern-est well used.
/// A picklist does not necessarily contain plate format info; this method guesses
/// based on the well furthest from A1 that exists in a transfer.
/// It's possible for a 1536 well plate to be seen as a 384 well if all of the wells
/// used could have fit in the top-left quadrant of a 384 well plate
/// (i.e. all lower than P24 in this case)
fn guess_plate_size(plate_filtered_records: &[&TransferRecord], which: PlateType) -> PlateFormat { fn guess_plate_size(plate_filtered_records: &[&TransferRecord], which: PlateType) -> PlateFormat {
let mut guess = PlateFormat::W96; // Never guess smaller than 96 let mut guess = PlateFormat::W96; // Never guess smaller than 96
for record in plate_filtered_records { for record in plate_filtered_records {
if let Some(Well { row, col }) = string_well_to_pt(match which { if let Some((row, col)) = string_well_to_pt(match which {
PlateType::Source => &record.source_well, PlateType::Source => &record.source_well,
PlateType::Destination => &record.destination_well, PlateType::Destination => &record.destination_well,
}) { }) {
@ -143,17 +116,6 @@ fn get_transfer_for_all_pairs(
transfers transfers
} }
/// Given a source and destination plate, get all of the wells transferred
/// from source to destination and turn this into a Transfer.
///
/// Note that even if you have what looks like two separate transfers,
/// this will join all transfers for a given source-dest pair.
/// For example:
/// Consider expanding all of src:A1 to the column dst:A
/// and src:B1 -> row dst:1
/// If you were making this transfer in plate tool, it would be easiest
/// to do this as two separate transfers.
/// On import, this would be seen as one transfer.
fn get_transfer_for_pair( fn get_transfer_for_pair(
records: &[TransferRecord], records: &[TransferRecord],
source: &PlateInstance, source: &PlateInstance,
@ -167,16 +129,12 @@ fn get_transfer_for_pair(
.filter(|x| x.source_plate == source_name && x.destination_plate == destination_name) .filter(|x| x.source_plate == source_name && x.destination_plate == destination_name)
.peekable(); .peekable();
filtered_records.peek()?; if filtered_records.peek().is_none() {
return None;
let mut source_wells: HashSet<Well> = HashSet::new(); }
let mut destination_wells: HashSet<Well> = HashSet::new();
// Volume Hash Maps
let mut source_only_volume_map: HashMap<Well, f32> = HashMap::new();
let mut destination_only_volume_map: HashMap<Well, f32> = HashMap::new();
let mut paired_volume_map: HashMap<(Well, Well), f32> = HashMap::new();
let mut source_wells: HashSet<(u8, u8)> = HashSet::new();
let mut destination_wells: HashSet<(u8, u8)> = HashSet::new();
for record in filtered_records { for record in filtered_records {
let source_point_opt = string_well_to_pt(&record.source_well); let source_point_opt = string_well_to_pt(&record.source_well);
let destination_point_opt = string_well_to_pt(&record.destination_well); let destination_point_opt = string_well_to_pt(&record.destination_well);
@ -187,23 +145,10 @@ fn get_transfer_for_pair(
source_wells.insert(source_point); source_wells.insert(source_point);
destination_wells.insert(destination_point); destination_wells.insert(destination_point);
source_only_volume_map.entry(source_point).and_modify(|v| *v += record.volume)
.or_insert(record.volume);
destination_only_volume_map.entry(destination_point).and_modify(|v| *v += record.volume)
.or_insert(record.volume);
paired_volume_map.entry((source_point, destination_point)).and_modify(|v| *v += record.volume)
.or_insert_with(|| record.volume); // No idea why this one needs to be different
} }
} }
let source_wells_vec: Vec<Well> = source_wells.into_iter().collect(); let source_wells_vec: Vec<(u8, u8)> = source_wells.into_iter().collect();
let destination_wells_vec: Vec<Well> = destination_wells.into_iter().collect(); let destination_wells_vec: Vec<(u8, u8)> = destination_wells.into_iter().collect();
let transfer_volume: TransferVolume = TransferVolume::WellMap(VolumeMaps {
source_only: source_only_volume_map,
destination_only: destination_only_volume_map,
paired: paired_volume_map,
});
let custom_region: Region = let custom_region: Region =
Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec)); Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec));
@ -219,13 +164,10 @@ fn get_transfer_for_pair(
let transfer_name = format!("{} to {}", source.name, destination.name); let transfer_name = format!("{} to {}", source.name, destination.name);
let mut transfer = Transfer::new( Some(Transfer::new(
source.clone(), source.clone(),
destination.clone(), destination.clone(),
transfer_region, transfer_region,
transfer_name, transfer_name,
); ))
transfer.volume = transfer_volume;
Some(transfer)
} }

View File

@ -1,11 +1,7 @@
use crate::transfer_volume::TransferVolume; use crate::transfer::Transfer;
use crate::util::*; use crate::util::*;
use crate::{transfer::Transfer, Well};
use super::{ use super::{alternative_formats::EchoClientTransferRecord, mangle_headers::mangle_headers, transfer_record::TransferRecordDeserializeIntermediate, TransferRecord};
alternative_formats::EchoClientTransferRecord, mangle_headers::mangle_headers,
transfer_record::TransferRecordDeserializeIntermediate, TransferRecord,
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use std::error::Error; use std::error::Error;
@ -19,32 +15,17 @@ pub fn transfer_to_records(
let map = tr.transfer_region.calculate_map(); let map = tr.transfer_region.calculate_map();
let mut records: Vec<TransferRecord> = vec![]; let mut records: Vec<TransferRecord> = vec![];
if let TransferVolume::WellMap(wm) = &tr.volume {
log::debug!("{:?}\n{}", wm.paired, wm.paired.len());
}
for s_well in source_wells { for s_well in source_wells {
let dest_wells = map(s_well); let dest_wells = map(s_well);
if let Some(dest_wells) = dest_wells { if let Some(dest_wells) = dest_wells {
for d_well in dest_wells { for d_well in dest_wells {
// Get volume here:
let volume: f32 = match &tr.volume {
TransferVolume::Single(x) => *x, // If all have the same value, use it everywhere
TransferVolume::WellMap(wm) => {
*wm.paired.get(&(s_well, d_well)).unwrap_or(&2.5f32)
}
};
records.push(TransferRecord { records.push(TransferRecord {
source_plate: src_barcode.to_string(), source_plate: src_barcode.to_string(),
source_well: format!("{}{}", num_to_letters(s_well.row).unwrap(), s_well.col), source_well: format!("{}{}", num_to_letters(s_well.0).unwrap(), s_well.1),
destination_plate: dest_barcode.to_string(), destination_plate: dest_barcode.to_string(),
destination_well: format!( destination_well: format!("{}{}", num_to_letters(d_well.0).unwrap(), d_well.1),
"{}{}", volume: tr.volume,
num_to_letters(d_well.row).unwrap(),
d_well.col
),
volume,
concentration: None, concentration: None,
}) })
} }
@ -71,19 +52,16 @@ pub fn records_to_echo_client_csv(trs: Vec<TransferRecord>) -> Result<String, Bo
Ok(data) Ok(data)
} }
/// Converts "spreadsheet format" well identification to coordinates pub fn string_well_to_pt(input: &str) -> Option<(u8, u8)> {
pub fn string_well_to_pt(input: &str) -> Option<Well> {
lazy_static! { lazy_static! {
static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap(); static ref REGEX: Regex = Regex::new(r"([A-Z,a-z]+)(\d+)").unwrap();
// Can this be removed?
static ref REGEX_ALT: Regex = Regex::new(r"(\d+)").unwrap(); static ref REGEX_ALT: Regex = Regex::new(r"(\d+)").unwrap();
} }
if let Some(c1) = REGEX.captures(input) { if let Some(c1) = REGEX.captures(input) {
if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) { if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) {
return Some(Well { row, col }); return Some((row, col))
} else { } else {
return None; return None
} }
} }
None None

View File

@ -1,15 +1,6 @@
//! This contains the `mangle_headers` function and its helpers
//!
//! Mangling headers is necessary to normalize CSV column names before
//! deserialization can be done by Serde.
//! Field detection occurs in `detect_field`.
//!
//! As of lib 0.4.0, unrecognized headers are ignored and left untouched by mangling.
//! However, they are not passed through to any subsequent exports from plate-tool.
pub fn mangle_headers(data: &str) -> String { pub fn mangle_headers(data: &str) -> String {
let (header, rows) = data.split_at(data.find('\n').unwrap()); let (header, rows) = data.split_at(data.find('\n').unwrap());
let fields = header.trim().split(','); let fields = header.trim().split(",");
let mut modified_headers: Vec<String> = Vec::new(); let mut modified_headers: Vec<String> = Vec::new();
for field in fields { for field in fields {
if let Some(f) = detect_field(field) { if let Some(f) = detect_field(field) {

View File

@ -4,6 +4,7 @@ mod auto;
mod mangle_headers; mod mangle_headers;
mod alternative_formats; mod alternative_formats;
pub use transfer_record::volume_default;
pub use transfer_record::TransferRecord; pub use transfer_record::TransferRecord;
pub use conversion::*; pub use conversion::*;

View File

@ -1,18 +1,14 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{plate::PlateFormat, util::num_to_letters}; use crate::{plate::PlateFormat, transfer::Transfer, util::num_to_letters};
/// Represents a single line of a CSV picklist.
/// In practice, this is generated from the deserialize intermediate.
/// A list of `TransferRecord`s can be used to create a `transfer::Transfer` struct;
/// see the `auto` module for this.
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct TransferRecord { pub struct TransferRecord {
#[serde(rename = "Source Barcode")] #[serde(rename = "Source Plate")]
pub source_plate: String, pub source_plate: String,
#[serde(rename = "Source Well")] #[serde(rename = "Source Well")]
pub source_well: String, pub source_well: String,
#[serde(rename = "Dest Barcode")] #[serde(rename = "Dest Plate")]
pub destination_plate: String, pub destination_plate: String,
#[serde(rename = "Destination Well")] #[serde(rename = "Destination Well")]
pub destination_well: String, pub destination_well: String,
@ -22,12 +18,6 @@ pub struct TransferRecord {
pub concentration: Option<f32>, pub concentration: Option<f32>,
} }
/// The deserialization intermediate is generated from a mangled CSV s.t.
/// the column headers are standardized to those given to the derive macro.
///
/// `TransferRecord` can *always* be generated from the intermediate.
/// Currently there's no reason why the inverse couldn't be true,
/// but there's no reason to convert back into the deserialize-only intermediate.
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct TransferRecordDeserializeIntermediate { pub struct TransferRecordDeserializeIntermediate {
#[serde(rename = "sourceplate")] #[serde(rename = "sourceplate")]
@ -74,7 +64,7 @@ impl From<TransferRecordDeserializeIntermediate> for TransferRecord {
} }
} }
let volume = value.volume.unwrap_or(2.5f32); let volume = value.volume.unwrap_or(volume_default());
TransferRecord { TransferRecord {
source_plate: value.source_plate, source_plate: value.source_plate,
@ -88,7 +78,6 @@ impl From<TransferRecordDeserializeIntermediate> for TransferRecord {
} }
impl TransferRecordDeserializeIntermediate { impl TransferRecordDeserializeIntermediate {
/// Used to cull malformed picklist entries that lack required information
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.source_plate.is_empty() self.source_plate.is_empty()
|| self.destination_plate.is_empty() || self.destination_plate.is_empty()
@ -105,3 +94,7 @@ fn numeric_well_to_alphanumeric(input: u16, pformat: PlateFormat) -> Option<Stri
Some(format!("{}{}", row_str, column)) Some(format!("{}{}", row_str, column))
} }
pub fn volume_default() -> f32 {
Transfer::default().volume
}

View File

@ -3,8 +3,4 @@ pub mod plate;
pub mod plate_instances; pub mod plate_instances;
pub mod transfer; pub mod transfer;
pub mod transfer_region; pub mod transfer_region;
pub mod transfer_volume;
pub mod util; pub mod util;
mod well;
pub use well::Well;

View File

@ -1,8 +1,5 @@
use crate::transfer_volume;
use super::plate_instances::*; use super::plate_instances::*;
use super::transfer_region::*; use super::transfer_region::*;
use super::transfer_volume::*;
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -17,7 +14,8 @@ pub struct Transfer {
#[serde(default = "Uuid::now_v7")] #[serde(default = "Uuid::now_v7")]
pub id: Uuid, pub id: Uuid,
pub transfer_region: TransferRegion, pub transfer_region: TransferRegion,
pub volume: TransferVolume, #[serde(default = "default_volume")]
pub volume: f32,
} }
impl Default for Transfer { impl Default for Transfer {
@ -28,11 +26,15 @@ impl Default for Transfer {
name: "New Transfer".to_string(), name: "New Transfer".to_string(),
id: Default::default(), id: Default::default(),
transfer_region: Default::default(), transfer_region: Default::default(),
volume: Default::default(), volume: default_volume(),
} }
} }
} }
fn default_volume() -> f32 {
2.5f32
}
impl Transfer { impl Transfer {
pub fn new( pub fn new(
source: PlateInstance, source: PlateInstance,
@ -46,7 +48,7 @@ impl Transfer {
name, name,
id: Uuid::now_v7(), id: Uuid::now_v7(),
transfer_region: tr, transfer_region: tr,
volume: transfer_volume::TransferVolume::Single(2.5), volume: 2.5,
} }
} }

View File

@ -1,32 +1,31 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::plate::Plate; use super::plate::Plate;
use crate::Well;
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub struct CustomRegion { pub struct CustomRegion {
src: Vec<Well>, src: Vec<(u8, u8)>,
dest: Vec<Well>, dest: Vec<(u8, u8)>,
} }
impl CustomRegion { impl CustomRegion {
pub fn new(src: Vec<Well>, dest: Vec<Well>) -> Self { pub fn new(src: Vec<(u8, u8)>, dest: Vec<(u8, u8)>) -> Self {
CustomRegion { src, dest } CustomRegion { src, dest }
} }
} }
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region { pub enum Region {
Rect(Well, Well), Rect((u8, u8), (u8, u8)),
Point(Well), Point((u8, u8)),
Custom(CustomRegion), Custom(CustomRegion),
} }
impl Default for Region { impl Default for Region {
fn default() -> Self { fn default() -> Self {
Region::Point(Well { row: 1, col: 1 }) Region::Point((1, 1))
} }
} }
impl TryFrom<Region> for (Well, Well) { impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
type Error = &'static str; type Error = &'static str;
fn try_from(region: Region) -> Result<Self, Self::Error> { fn try_from(region: Region) -> Result<Self, Self::Error> {
if let Region::Rect(c1, c2) = region { if let Region::Rect(c1, c2) = region {
@ -38,10 +37,12 @@ impl TryFrom<Region> for (Well, Well) {
} }
} }
type Corner = (u8, u8);
type Rectangle = (Corner, Corner);
impl Region { impl Region {
pub fn new_custom(transfers: &Vec<(Well, Well)>) -> Self { pub fn new_custom(transfers: &Vec<Rectangle>) -> Self {
let mut src_pts: Vec<Well> = Vec::with_capacity(transfers.len()); let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
let mut dest_pts: Vec<Well> = Vec::with_capacity(transfers.len()); let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len());
for transfer in transfers { for transfer in transfers {
src_pts.push(transfer.0); src_pts.push(transfer.0);
@ -79,10 +80,10 @@ impl Default for TransferRegion {
} }
impl TransferRegion { impl TransferRegion {
pub fn get_source_wells(&self) -> Vec<Well> { pub fn get_source_wells(&self) -> Vec<(u8, u8)> {
match &self.source_region { match &self.source_region {
Region::Rect(c1, c2) => { Region::Rect(c1, c2) => {
let mut wells = Vec::<Well>::new(); let mut wells = Vec::<(u8, u8)>::new();
let (ul, br) = standardize_rectangle(c1, c2); let (ul, br) = standardize_rectangle(c1, c2);
let (interleave_i, interleave_j) = self.interleave_source; let (interleave_i, interleave_j) = self.interleave_source;
// NOTE: This will panic if either is 0! // NOTE: This will panic if either is 0!
@ -92,12 +93,12 @@ impl TransferRegion {
let (interleave_i, interleave_j) = let (interleave_i, interleave_j) =
(i8::max(interleave_i, 1), i8::max(interleave_j, 1)); (i8::max(interleave_i, 1), i8::max(interleave_j, 1));
for i in (ul.row..=br.row).step_by(i8::abs(interleave_i) as usize) { for i in (ul.0..=br.0).step_by(i8::abs(interleave_i) as usize) {
for j in (ul.col..=br.col).step_by(i8::abs(interleave_j) as usize) { for j in (ul.1..=br.1).step_by(i8::abs(interleave_j) as usize) {
// NOTE: It looks like we're ignoring negative interleaves, // NOTE: It looks like we're ignoring negative interleaves,
// because it wouldn't make a difference here---the same // because it wouldn't make a difference here---the same
// wells will still be involved in the transfer. // wells will still be involved in the transfer.
wells.push(Well { row: i, col: j }) wells.push((i, j))
} }
} }
wells wells
@ -107,14 +108,14 @@ impl TransferRegion {
} }
} }
pub fn get_destination_wells(&self) -> Vec<Well> { pub fn get_destination_wells(&self) -> Vec<(u8, u8)> {
match &self.source_region { match &self.source_region {
Region::Custom(c) => c.dest.clone(), Region::Custom(c) => c.dest.clone(),
_ => { _ => {
let map = self.calculate_map(); let map = self.calculate_map();
let source_wells = self.get_source_wells(); let source_wells = self.get_source_wells();
let mut wells = Vec::<Well>::new(); let mut wells = Vec::<(u8, u8)>::new();
for well in source_wells { for well in source_wells {
if let Some(mut dest_wells) = map(well) { if let Some(mut dest_wells) = map(well) {
@ -128,14 +129,14 @@ impl TransferRegion {
} }
#[allow(clippy::type_complexity)] // Resolving gives inherent associated type error #[allow(clippy::type_complexity)] // Resolving gives inherent associated type error
pub fn calculate_map(&self) -> Box<dyn Fn(Well) -> Option<Vec<Well>> + '_> { pub fn calculate_map(&self) -> Box<dyn Fn((u8, u8)) -> Option<Vec<(u8, u8)>> + '_> {
// By validating first, we have a stronger guarantee that // By validating first, we have a stronger guarantee that
// this function will not panic. :) // this function will not panic. :)
// log::debug!("Validating: {:?}", self.validate()); // log::debug!("Validating: {:?}", self.validate());
if let Err(msg) = self.validate() { if let Err(msg) = self.validate() {
eprintln!("{}", msg); eprintln!("{}", msg);
eprintln!("This transfer will be empty."); eprintln!("This transfer will be empty.");
return Box::new(|_| None); return Box::new(|(_, _)| None);
} }
// log::debug!("What is ild? {:?}", self); // log::debug!("What is ild? {:?}", self);
@ -143,10 +144,10 @@ impl TransferRegion {
let il_dest = self.interleave_dest; let il_dest = self.interleave_dest;
let il_source = self.interleave_source; let il_source = self.interleave_source;
let source_corners: (Well, Well) = match self.source_region { let source_corners: ((u8, u8), (u8, u8)) = match self.source_region {
Region::Point(w) => (w, w), Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2), Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => (Well { row: 0, col: 0 }, Well { row: 0, col: 0 }), Region::Custom(_) => ((0, 0), (0, 0)),
}; };
let (source_ul, _) = standardize_rectangle(&source_corners.0, &source_corners.1); let (source_ul, _) = standardize_rectangle(&source_corners.0, &source_corners.1);
// This map is not necessarily injective or surjective, // This map is not necessarily injective or surjective,
@ -156,44 +157,43 @@ impl TransferRegion {
// Non-replicate transfers: // Non-replicate transfers:
match &self.dest_region { match &self.dest_region {
Region::Point(Well { row: x, col: y }) => { Region::Point((x, y)) => {
Box::new(move |Well { row: i, col: j }| { Box::new(move |(i, j)| {
if source_wells.contains(&Well { row: i, col: j }) { if source_wells.contains(&(i, j)) {
// Validity here already checked by self.validate() // Validity here already checked by self.validate()
Some(vec![Well { Some(vec![(
row: x + i x + i
.checked_sub(source_ul.row) .checked_sub(source_ul.0)
.expect("Point cannot have been less than UL") .expect("Point cannot have been less than UL")
.checked_div(il_source.0.unsigned_abs()) .checked_div(il_source.0.unsigned_abs())
.expect("Source interleave cannot be 0") .expect("Source interleave cannot be 0")
.mul(il_dest.0.unsigned_abs()), .mul(il_dest.0.unsigned_abs()),
col: y + j y + j
.checked_sub(source_ul.col) .checked_sub(source_ul.1)
.expect("Point cannot have been less than UL") .expect("Point cannot have been less than UL")
.checked_div(il_source.1.unsigned_abs()) .checked_div(il_source.1.unsigned_abs())
.expect("Source interleave cannot be 0") .expect("Source interleave cannot be 0")
.mul(il_dest.1.unsigned_abs()), .mul(il_dest.1.unsigned_abs()),
}]) )])
} else { } else {
None None
} }
}) })
} }
Region::Rect(c1, c2) => { Region::Rect(c1, c2) => {
Box::new(move |w| { Box::new(move |(i, j)| {
let Well { row: i, col: j } = w; if source_wells.contains(&(i, j)) {
if source_wells.contains(&w) {
let possible_destination_wells = create_dense_rectangle(c1, c2); let possible_destination_wells = create_dense_rectangle(c1, c2);
let (d_ul, d_br) = standardize_rectangle(c1, c2); let (d_ul, d_br) = standardize_rectangle(c1, c2);
let (s_ul, s_br) = let (s_ul, s_br) =
standardize_rectangle(&source_corners.0, &source_corners.1); standardize_rectangle(&source_corners.0, &source_corners.1);
let s_dims = ( let s_dims = (
s_br.row.checked_sub(s_ul.row).unwrap() + 1, s_br.0.checked_sub(s_ul.0).unwrap() + 1,
s_br.col.checked_sub(s_ul.col).unwrap() + 1, s_br.1.checked_sub(s_ul.1).unwrap() + 1,
); );
let d_dims = ( let d_dims = (
d_br.row.checked_sub(d_ul.row).unwrap() + 1, d_br.0.checked_sub(d_ul.0).unwrap() + 1,
d_br.col.checked_sub(d_ul.col).unwrap() + 1, d_br.1.checked_sub(d_ul.1).unwrap() + 1,
); );
let number_used_src_wells = ( let number_used_src_wells = (
// Number of used source wells // Number of used source wells
@ -222,34 +222,34 @@ impl TransferRegion {
.unwrap() as u8, .unwrap() as u8,
); );
let i = i let i = i
.saturating_sub(s_ul.row) .saturating_sub(s_ul.0)
.saturating_div(il_source.0.unsigned_abs()); .saturating_div(il_source.0.unsigned_abs());
let j = j let j = j
.saturating_sub(s_ul.col) .saturating_sub(s_ul.1)
.saturating_div(il_source.1.unsigned_abs()); .saturating_div(il_source.1.unsigned_abs());
Some( Some(
possible_destination_wells possible_destination_wells
.into_iter() .into_iter()
.filter(|Well { row: x , ..}| { .filter(|(x, _)| {
x.checked_sub(d_ul.row).unwrap() x.checked_sub(d_ul.0).unwrap()
% (number_used_src_wells.0 * il_dest.0.unsigned_abs()) // Counter along x % (number_used_src_wells.0 * il_dest.0.unsigned_abs()) // Counter along x
== (il_dest.0.unsigned_abs() *i) == (il_dest.0.unsigned_abs() *i)
% (number_used_src_wells.0 * il_dest.0.unsigned_abs()) % (number_used_src_wells.0 * il_dest.0.unsigned_abs())
}) })
.filter(|Well { col: y, .. }| { .filter(|(_, y)| {
y.checked_sub(d_ul.col).unwrap() y.checked_sub(d_ul.1).unwrap()
% (number_used_src_wells.1 * il_dest.1.unsigned_abs()) // Counter along u % (number_used_src_wells.1 * il_dest.1.unsigned_abs()) // Counter along u
== (il_dest.1.unsigned_abs() *j) == (il_dest.1.unsigned_abs() *j)
% (number_used_src_wells.1 * il_dest.1.unsigned_abs()) % (number_used_src_wells.1 * il_dest.1.unsigned_abs())
}) })
.filter(|Well { row: x, col: y }| { .filter(|(x, y)| {
// How many times have we replicated? < How many are we allowed // How many times have we replicated? < How many are we allowed
// to replicate? // to replicate?
x.checked_sub(d_ul.row).unwrap().div_euclid( x.checked_sub(d_ul.0).unwrap().div_euclid(
number_used_src_wells.0 * il_dest.0.unsigned_abs(), number_used_src_wells.0 * il_dest.0.unsigned_abs(),
) < count.0 ) < count.0
&& y.checked_sub(d_ul.col).unwrap().div_euclid( && y.checked_sub(d_ul.1).unwrap().div_euclid(
number_used_src_wells.1 * il_dest.1.unsigned_abs(), number_used_src_wells.1 * il_dest.1.unsigned_abs(),
) < count.1 ) < count.1
}) })
@ -260,14 +260,14 @@ impl TransferRegion {
} }
}) })
} }
Region::Custom(c) => Box::new(move |Well { row: i, col: j }| { Region::Custom(c) => Box::new(move |(i, j)| {
let src = c.src.clone(); let src = c.src.clone();
let dest = c.dest.clone(); let dest = c.dest.clone();
let points: Vec<Well> = src let points: Vec<(u8, u8)> = src
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_index, Well { row: x, col: y })| *x == i && *y == j) .filter(|(_index, (x, y))| *x == i && *y == j)
.map(|(index, _)| dest[index]) .map(|(index, _)| dest[index])
.collect(); .collect();
if points.is_empty() { if points.is_empty() {
@ -296,15 +296,15 @@ impl TransferRegion {
// later // later
Region::Rect(s1, s2) => { Region::Rect(s1, s2) => {
// Check if all source wells exist: // Check if all source wells exist:
if s1.row == 0 || s1.col == 0 || s2.row == 0 || s2.col == 0 { if s1.0 == 0 || s1.1 == 0 || s2.0 == 0 || s2.1 == 0 {
return Err("Source region is out-of-bounds! (Too small)"); return Err("Source region is out-of-bounds! (Too small)");
} }
// Sufficient to check if the corners are in-bounds // Sufficient to check if the corners are in-bounds
let source_max = self.source_plate.size(); let source_max = self.source_plate.size();
if s1.row > source_max.0 || s2.row > source_max.0 { if s1.0 > source_max.0 || s2.0 > source_max.0 {
return Err("Source region is out-of-bounds! (Too tall)"); return Err("Source region is out-of-bounds! (Too tall)");
} }
if s1.col > source_max.1 || s2.col > source_max.1 { if s1.1 > source_max.1 || s2.1 > source_max.1 {
// log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1); // log::debug!("s1.1: {}, max.1: {}", s1.1, source_max.1);
return Err("Source region is out-of-bounds! (Too wide)"); return Err("Source region is out-of-bounds! (Too wide)");
} }
@ -325,34 +325,28 @@ impl TransferRegion {
} }
} }
fn create_dense_rectangle(c1: &Well, c2: &Well) -> Vec<Well> { fn create_dense_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> Vec<(u8, u8)> {
// Creates a vector of every point between two corners // Creates a vector of every point between two corners
let (c1, c2) = standardize_rectangle(c1, c2); let (c1, c2) = standardize_rectangle(c1, c2);
let mut points = Vec::<Well>::new(); let mut points = Vec::<(u8, u8)>::new();
for i in c1.row..=c2.row { for i in c1.0..=c2.0 {
for j in c1.col..=c2.col { for j in c1.1..=c2.1 {
points.push(Well { row: i, col: j }); points.push((i, j));
} }
} }
points points
} }
fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) { fn standardize_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> ((u8, u8), (u8, u8)) {
let upper_left_i = u8::min(c1.row, c2.row); let upper_left_i = u8::min(c1.0, c2.0);
let upper_left_j = u8::min(c1.col, c2.col); let upper_left_j = u8::min(c1.1, c2.1);
let bottom_right_i = u8::max(c1.row, c2.row); let bottom_right_i = u8::max(c1.0, c2.0);
let bottom_right_j = u8::max(c1.col, c2.col); let bottom_right_j = u8::max(c1.1, c2.1);
( (
Well { (upper_left_i, upper_left_j),
row: upper_left_i, (bottom_right_i, bottom_right_j),
col: upper_left_j,
},
Well {
row: bottom_right_i,
col: bottom_right_j,
},
) )
} }
@ -369,7 +363,7 @@ impl fmt::Display for TransferRegion {
let mut source_string = String::new(); let mut source_string = String::new();
for i in 1..=source_dims.0 { for i in 1..=source_dims.0 {
for j in 1..=source_dims.1 { for j in 1..=source_dims.1 {
if source_wells.contains(&Well { row: i, col: j }) { if source_wells.contains(&(i, j)) {
source_string.push('x') source_string.push('x')
} else { } else {
source_string.push('.') source_string.push('.')
@ -385,7 +379,7 @@ impl fmt::Display for TransferRegion {
let mut dest_string = String::new(); let mut dest_string = String::new();
for i in 1..=dest_dims.0 { for i in 1..=dest_dims.0 {
for j in 1..=dest_dims.1 { for j in 1..=dest_dims.1 {
if dest_wells.contains(&Well { row: i, col: j }) { if dest_wells.contains(&(i, j)) {
dest_string.push('x') dest_string.push('x')
} else { } else {
dest_string.push('.') dest_string.push('.')
@ -399,130 +393,134 @@ impl fmt::Display for TransferRegion {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::plate::*; use wasm_bindgen_test::*;
use crate::transfer_region::*;
use crate::data::plate::*;
use crate::data::transfer_region::*;
#[test] #[test]
#[wasm_bindgen_test]
fn test_simple_transfer() { fn test_simple_transfer() {
let source = Plate::new(PlateType::Source, PlateFormat::W96); let source = Plate::new(PlateType::Source, PlateFormat::W96);
let destination = Plate::new(PlateType::Destination, PlateFormat::W384); let destination = Plate::new(PlateType::Destination, PlateFormat::W384);
let transfer1 = TransferRegion { let transfer1 = TransferRegion {
source_plate: source, source_plate: source,
source_region: Region::Rect(Well{ row: 1, col: 1}, Well { row: 1, col: 1 }), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point(Well {row: 3, col: 3}), dest_region: Region::Point((3, 3)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (1, 1), interleave_dest: (1, 1),
}; };
let transfer1_map = transfer1.calculate_map(); let transfer1_map = transfer1.calculate_map();
assert_eq!( assert_eq!(
transfer1_map(Well {row: 1, col: 1}), transfer1_map((1, 1)),
Some(vec! {Well {row: 3, col: 3}}), Some(vec! {(3,3)}),
"Failed basic shift transfer 1" "Failed basic shift transfer 1"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 1, col: 2}), transfer1_map((1, 2)),
Some(vec! {Well{ row: 3, col: 4}}), Some(vec! {(3,4)}),
"Failed basic shift transfer 2" "Failed basic shift transfer 2"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 2, col: 2}), transfer1_map((2, 2)),
Some(vec! {Well{ row: 4, col: 4}}), Some(vec! {(4,4)}),
"Failed basic shift transfer 3" "Failed basic shift transfer 3"
); );
let transfer2 = TransferRegion { let transfer2 = TransferRegion {
source_plate: source, source_plate: source,
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 3, col: 3}), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point(Well{ row: 3, col: 3}), dest_region: Region::Point((3, 3)),
interleave_source: (2, 2), interleave_source: (2, 2),
interleave_dest: (1, 1), interleave_dest: (1, 1),
}; };
let transfer2_map = transfer2.calculate_map(); let transfer2_map = transfer2.calculate_map();
assert_eq!( assert_eq!(
transfer2_map(Well{ row: 1, col: 1}), transfer2_map((1, 1)),
Some(vec! {Well{ row: 3, col: 3}}), Some(vec! {(3,3)}),
"Failed source interleave, type simple 1" "Failed source interleave, type simple 1"
); );
assert_eq!( assert_eq!(
transfer2_map(Well{ row: 1, col: 2}), transfer2_map((1, 2)),
None, None,
"Failed source interleave, type simple 2" "Failed source interleave, type simple 2"
); );
assert_eq!( assert_eq!(
transfer2_map(Well{ row: 2, col: 2}), transfer2_map((2, 2)),
None, None,
"Failed source interleave, type simple 3" "Failed source interleave, type simple 3"
); );
assert_eq!( assert_eq!(
transfer2_map(Well{ row: 3, col: 3}), transfer2_map((3, 3)),
Some(vec! {Well{ row: 4, col: 4}}), Some(vec! {(4,4)}),
"Failed source interleave, type simple 4" "Failed source interleave, type simple 4"
); );
let transfer3 = TransferRegion { let transfer3 = TransferRegion {
source_plate: source, source_plate: source,
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 3, col: 3}), source_region: Region::Rect((1, 1), (3, 3)),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point(Well{ row: 3, col: 3}), dest_region: Region::Point((3, 3)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (2, 3), interleave_dest: (2, 3),
}; };
let transfer3_map = transfer3.calculate_map(); let transfer3_map = transfer3.calculate_map();
assert_eq!( assert_eq!(
transfer3_map(Well{ row: 1, col: 1}), transfer3_map((1, 1)),
Some(vec! {Well{ row: 3, col: 3}}), Some(vec! {(3,3)}),
"Failed destination interleave, type simple 1" "Failed destination interleave, type simple 1"
); );
assert_eq!( assert_eq!(
transfer3_map(Well{ row: 2, col: 1}), transfer3_map((2, 1)),
Some(vec! {Well{ row: 5, col: 3}}), Some(vec! {(5,3)}),
"Failed destination interleave, type simple 2" "Failed destination interleave, type simple 2"
); );
assert_eq!( assert_eq!(
transfer3_map(Well{ row: 1, col: 2}), transfer3_map((1, 2)),
Some(vec! {Well{ row: 3, col: 6}}), Some(vec! {(3,6)}),
"Failed destination interleave, type simple 3" "Failed destination interleave, type simple 3"
); );
assert_eq!( assert_eq!(
transfer3_map(Well{ row: 2, col: 2}), transfer3_map((2, 2)),
Some(vec! {Well{ row: 5, col: 6}}), Some(vec! {(5,6)}),
"Failed destination interleave, type simple 4" "Failed destination interleave, type simple 4"
); );
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_replicate_transfer() { fn test_replicate_transfer() {
let source = Plate::new(PlateType::Source, PlateFormat::W96); let source = Plate::new(PlateType::Source, PlateFormat::W96);
let destination = Plate::new(PlateType::Destination, PlateFormat::W384); let destination = Plate::new(PlateType::Destination, PlateFormat::W384);
let transfer1 = TransferRegion { let transfer1 = TransferRegion {
source_plate: source, source_plate: source,
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 2, col: 2}), source_region: Region::Rect((1, 1), (2, 2)),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Rect(Well{ row: 2, col: 2}, Well{row:11, col:11}), dest_region: Region::Rect((2, 2), (11, 11)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (3, 3), interleave_dest: (3, 3),
}; };
let transfer1_map = transfer1.calculate_map(); let transfer1_map = transfer1.calculate_map();
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 1, col: 1}), transfer1_map((1, 1)),
Some(vec! {Well{ row: 2, col: 2}, Well{ row: 2, col: 8}, Well{ row: 8, col: 2}, Well{ row: 8, col: 8}}), Some(vec! {(2, 2), (2, 8), (8, 2), (8, 8)}),
"Failed type replicate 1" "Failed type replicate 1"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 2, col: 1}), transfer1_map((2, 1)),
Some(vec! {Well{ row: 5, col: 2}, Well{ row: 5, col: 8}, Well{ row: 11, col: 2}, Well{ row: 11, col: 8}}), Some(vec! {(5, 2), (5, 8), (11, 2), (11, 8)}),
"Failed type replicate 1" "Failed type replicate 1"
); );
let transfer2 = TransferRegion { let transfer2 = TransferRegion {
source_plate: Plate::new(PlateType::Source, PlateFormat::W384), source_plate: Plate::new(PlateType::Source, PlateFormat::W384),
dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384), dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384),
source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 2, col: 3}), source_region: Region::Rect((1, 1), (2, 3)),
dest_region: Region::Rect(Well{ row: 2, col: 2}, Well{ row: 11, col: 16}), dest_region: Region::Rect((2, 2), (11, 16)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (2, 2), interleave_dest: (2, 2),
}; };
@ -530,69 +528,71 @@ mod tests {
let transfer2_dest = transfer2.get_destination_wells(); let transfer2_dest = transfer2.get_destination_wells();
assert_eq!( assert_eq!(
transfer2_source, transfer2_source,
vec![Well{ row: 1, col: 1}, Well{ row: 1, col: 2}, Well{ row: 1, col: 3}, Well{ row: 2, col: 1}, Well{ row: 2, col: 2}, Well{ row: 2, col: 3}], vec![(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)],
"Failed type replicate 2 source" "Failed type replicate 2 source"
); );
assert_eq!( assert_eq!(
transfer2_dest, transfer2_dest,
vec![ vec![
Well{ row: 2, col: 2}, (2, 2),
Well{ row: 2, col: 8}, (2, 8),
Well{ row: 6, col: 2}, (6, 2),
Well{ row: 6, col: 8}, (6, 8),
Well{ row: 2, col: 4}, (2, 4),
Well{ row: 2, col: 10}, (2, 10),
Well{ row: 6, col: 4}, (6, 4),
Well{ row: 6, col: 10}, (6, 10),
Well{ row: 2, col: 6}, (2, 6),
Well{ row: 2, col: 12}, (2, 12),
Well{ row: 6, col: 6}, (6, 6),
Well{ row: 6, col: 12}, (6, 12),
Well{ row: 4, col: 2}, (4, 2),
Well{ row: 4, col: 8}, (4, 8),
Well{ row: 8, col: 2}, (8, 2),
Well{ row: 8, col: 8}, (8, 8),
Well{ row: 4, col: 4}, (4, 4),
Well{ row: 4, col: 10}, (4, 10),
Well{ row: 8, col: 4}, (8, 4),
Well{ row: 8, col: 10}, (8, 10),
Well{ row: 4, col: 6}, (4, 6),
Well{ row: 4, col: 12}, (4, 12),
Well{ row: 8, col: 6}, (8, 6),
Well{ row: 8, col: 12} (8, 12)
], ],
"Failed type replicate 2 destination" "Failed type replicate 2 destination"
); );
} }
#[test] #[test]
#[wasm_bindgen_test]
fn test_pooling_transfer() { fn test_pooling_transfer() {
use std::collections::HashSet;
let transfer1 = TransferRegion { let transfer1 = TransferRegion {
source_plate: Plate::new(PlateType::Source, PlateFormat::W384), source_plate: Plate::new(PlateType::Source, PlateFormat::W384),
dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384), dest_plate: Plate::new(PlateType::Destination, PlateFormat::W384),
source_region: Region::Rect(Well{ row: 1, col: 4}, Well{ row: 3, col: 7}), source_region: Region::Rect((1, 4), (3, 7)),
dest_region: Region::Point(Well{ row: 1, col: 9}), dest_region: Region::Point((1, 9)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (0, 2), interleave_dest: (0, 2),
}; };
//let transfer1_source = transfer1.get_source_wells(); //let transfer1_source = transfer1.get_source_wells();
let transfer1_dest: HashSet<Well> = transfer1.get_destination_wells().into_iter().collect(); let mut transfer1_dest = transfer1.get_destination_wells();
transfer1_dest.sort();
transfer1_dest.dedup(); // Makes our check easier, otherwise we have repeated wells
let transfer1_map = transfer1.calculate_map(); let transfer1_map = transfer1.calculate_map();
// Skipping source check---it's just 12 wells. // Skipping source check---it's just 12 wells.
assert_eq!( assert_eq!(
transfer1_dest, transfer1_dest,
vec![Well{ row: 1, col: 9}, Well{ row: 1, col: 11}, Well{ row: 1, col: 13}, Well{ row: 1, col: 15}].into_iter().collect(), vec![(1, 9), (1, 11), (1, 13), (1, 15)],
"Failed type pool 1 dest" "Failed type pool 1 dest"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 2, col: 6}), transfer1_map((2, 6)),
Some(vec![Well{ row: 1, col: 13}]), Some(vec![(1, 13)]),
"Failed type pool 1 map 1" "Failed type pool 1 map 1"
); );
assert_eq!( assert_eq!(
transfer1_map(Well{ row: 3, col: 7}), transfer1_map((3, 7)),
Some(vec![Well{ row: 1, col: 15}]), Some(vec![(1, 15)]),
"Failed type pool 1 map 2" "Failed type pool 1 map 2"
); );
} }

View File

@ -1,30 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::Well;
type WellPair = (Well, Well);
#[non_exhaustive]
#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
pub enum TransferVolume {
Single(f32),
WellMap(VolumeMaps),
}
#[serde_with::serde_as]
#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
pub struct VolumeMaps {
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub source_only: HashMap<Well, f32>,
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub destination_only: HashMap<Well, f32>,
#[serde_as(as = "HashMap<serde_with::json::JsonString, _>")]
pub paired: HashMap<WellPair, f32>,
}
impl Default for TransferVolume {
fn default() -> Self {
TransferVolume::Single(2.5f32)
}
}

View File

@ -1,9 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Debug, Hash)]
pub struct Well {
/// Top to bottom assuming landscape, i.e. lettered
pub row: u8,
/// Left to right assuming landscape, i.e. numbered
pub col: u8,
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "plate-tool-web" name = "plate-tool-web"
version = "0.5.0" version = "0.4.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,27 +0,0 @@
@use "sass:color";
@use "./variables" as *;
$selection-border-width: 2px;
@mixin standard_button {
color: inherit;
display: inline;
margin-left: 3px;
border: 2px solid rgba(1,1,1,0.2);
background: transparent;
user-select: none;
list-style: none;
line-height: 1em;
&::before {
text-align: center;
vertical-align: middle;
}
&:hover {
background: color.change($color-light, $alpha: 0.08);
transition: background 0.1s;
border: $selection-border-width solid color.change($color-light, $alpha:0.3);
}
}

View File

@ -2,8 +2,6 @@
@use "../variables" as *; @use "../variables" as *;
div.plate_container { div.plate_container {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-evenly; justify-content: space-evenly;
@ -40,22 +38,4 @@ div.plate_container {
grid-row: 2; grid-row: 2;
} }
} }
div.plate_container--heatmap-notice {
position: absolute;
top: 0.5em;
left: 0.5em;
animation: 1s 1 attention_on_load;
}
}
@keyframes attention_on_load {
from{
color: $color-light;
transform: scale(1.05);
}
to {
color: inherit;
}
} }

View File

@ -1,6 +1,5 @@
@use "sass:color"; @use "sass:color";
@use "../variables" as *; @use "../variables" as *;
@use "../button" as *;
div.transfer_menu { div.transfer_menu {
position: relative; position: relative;
@ -41,11 +40,6 @@ div.transfer_menu {
} }
} }
div.transfer_menu input[type="button"] {
@include standard_button;
}
input { input {
text-align: center; text-align: center;
margin-left: 0.5em; margin-left: 0.5em;
@ -56,10 +50,10 @@ input {
padding: 0; padding: 0;
&[type="text"] { &[type="text"] {
width: 6em; width: 4em;
} }
&[name="name"] { &[name="name"] {
width: calc(100% - 6em); // Override above width: 6em; // Override above
} }
&[type="number"] { &[type="number"] {
width: 2em; width: 2em;

View File

@ -1,6 +1,5 @@
@use "sass:color"; @use "sass:color";
@use "../variables" as *; @use "../variables" as *;
@use "../button" as *;
$selection-border-width: 2px; $selection-border-width: 2px;
@ -54,17 +53,3 @@ div.tree li {
background: color.change($color-light, $alpha: 0.2); background: color.change($color-light, $alpha: 0.2);
} }
} }
div.tree--header {
margin-top: 1%;
display: flex;
align-items: center;
}
div.tree--header button {
@include standard_button;
&::before {
content: "Add";
}
}

View File

@ -12,7 +12,6 @@ use crate::components::states::MainState;
use plate_tool_lib::transfer::Transfer; use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::{Region, TransferRegion}; use plate_tool_lib::transfer_region::{Region, TransferRegion};
use plate_tool_lib::Well;
use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord}; use plate_tool_lib::csv::{auto, string_well_to_pt, TransferRecord};
@ -239,7 +238,7 @@ pub fn import_transfer_csv_submit_callback(
let from_dest = from_dest.value(); let from_dest = from_dest.value();
let to_dest = to_dest.value(); let to_dest = to_dest.value();
let records: Vec<(Well, Well)> = records let records: Vec<((u8, u8), (u8, u8))> = records
.iter() .iter()
.filter(|record| record.source_plate == from_source) .filter(|record| record.source_plate == from_source)
.filter(|record| record.destination_plate == from_dest) .filter(|record| record.destination_plate == from_dest)
@ -296,11 +295,11 @@ fn auto_callback(
Closure::<dyn FnMut(_)>::new(move |_| { Closure::<dyn FnMut(_)>::new(move |_| {
let res = auto(&records); let res = auto(&records);
main_dispatch.reduce_mut(|state| { main_dispatch.reduce_mut(|state| {
state.source_plates.extend(res.sources); state.source_plates.extend(res.sources.into_iter());
state state
.destination_plates .destination_plates
.extend(res.destinations); .extend(res.destinations.into_iter());
state.transfers.extend(res.transfers); state.transfers.extend(res.transfers.into_iter());
}); });
}) })
} }

View File

@ -9,6 +9,8 @@ use crate::components::states::MainState;
use super::create_close_button; use super::create_close_button;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn input_json_input_callback( pub fn input_json_input_callback(
main_dispatch: Dispatch<MainState>, main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement, modal: HtmlDialogElement,

View File

@ -1,27 +1,35 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlDialogElement, HtmlElement, HtmlFormElement, HtmlInputElement, Url,
};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::{new_plate_dialog::{NewPlateDialogType}, states::{CurrentTransfer, MainState}}; use crate::components::states::{CurrentTransfer, MainState};
use crate::state_to_csv;
type NoParamsCallback = Box<dyn Fn(())>; type NoParamsCallback = Box<dyn Fn(())>;
pub fn new_plate_dialog_callback( pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>, new_plate_dialog_is_open: UseStateHandle<bool>,
) -> NoParamsCallback { ) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone(); let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| { Box::new(move |_| {
new_plate_dialog_is_open.set(None); new_plate_dialog_is_open.set(false);
}) })
} }
pub fn open_new_plate_dialog_callback( pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>> new_plate_dialog_is_open: UseStateHandle<bool>,
) -> Box<dyn Fn(NewPlateDialogType)> { ) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone(); let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |dt| { Box::new(move |_| {
new_plate_dialog_is_open.set(Some(dt)); new_plate_dialog_is_open.set(true);
}) })
} }

View File

@ -1,9 +1,10 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use js_sys::Function;
use wasm_bindgen::{prelude::*, JsCast}; use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{ use web_sys::{
HtmlDialogElement, HtmlFormElement, HtmlOptionElement, HtmlSelectElement, FileReader, HtmlButtonElement, HtmlDialogElement, HtmlElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement,
}; };
use yew::prelude::*; use yew::prelude::*;
@ -13,6 +14,8 @@ use crate::components::states::{CsvExportType, MainState};
use super::create_close_button; use super::create_close_button;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn toggle_in_transfer_hashes_callback( pub fn toggle_in_transfer_hashes_callback(
main_dispatch: Dispatch<MainState>, main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> { ) -> Callback<web_sys::MouseEvent> {

View File

@ -4,6 +4,8 @@ use wasm_bindgen::{prelude::*, JsCast};
use web_sys::HtmlElement; use web_sys::HtmlElement;
use yew::prelude::*; use yew::prelude::*;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn create_close_button(close: &Closure<dyn FnMut(Event)>) -> HtmlElement { pub fn create_close_button(close: &Closure<dyn FnMut(Event)>) -> HtmlElement {
let document = web_sys::window().unwrap().document().unwrap(); let document = web_sys::window().unwrap().document().unwrap();
let close_button = document let close_button = document

View File

@ -6,7 +6,7 @@ use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::transfer_menu::RegionDisplay; use crate::components::transfer_menu::RegionDisplay;
use plate_tool_lib::{transfer::Transfer, transfer_region::Region, transfer_volume::TransferVolume}; use plate_tool_lib::{transfer::Transfer, transfer_region::Region};
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::states::{CurrentTransfer, MainState};
@ -142,7 +142,7 @@ pub fn on_volume_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Call
.expect("Must have been emitted by input"); .expect("Must have been emitted by input");
if let Ok(num) = input.value().parse::<f32>() { if let Ok(num) = input.value().parse::<f32>() {
ct_dispatch.reduce_mut(|state| { ct_dispatch.reduce_mut(|state| {
state.transfer.volume = TransferVolume::Single(num); state.transfer.volume = num;
}); });
} }
}) })
@ -184,18 +184,12 @@ pub fn save_transfer_button_callback_callback(
.iter() .iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate) .find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
{ {
// Only mutable for volume assignment! let new_transfer = Transfer::new(
let mut new_transfer = Transfer::new(
spi.clone(), spi.clone(),
dpi.clone(), dpi.clone(),
ct_state.transfer.transfer_region.clone(), ct_state.transfer.transfer_region.clone(),
ct_state.transfer.name.clone(), ct_state.transfer.name.clone(),
); );
// This clone is cheap - we know this is just a single value
// so cloning f32 will be fast.
new_transfer.volume = ct_state.transfer.volume.clone();
main_dispatch.reduce_mut(|state| { main_dispatch.reduce_mut(|state| {
state.transfers.push(new_transfer); state.transfers.push(new_transfer);
state.selected_transfer = state state.selected_transfer = state

View File

@ -1,8 +1,8 @@
use plate_tool_lib::{plate::PlateType}; use plate_tool_lib::{plate::PlateType, plate_instances::PlateInstance};
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::{prelude::*, JsCast}; use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{ use web_sys::{
HtmlDialogElement, HtmlElement, HtmlOptionElement, HtmlDialogElement, HtmlElement, HtmlFormElement, HtmlInputElement, HtmlOptionElement,
HtmlSelectElement, HtmlSelectElement,
}; };
@ -203,7 +203,7 @@ fn find_plate_type(main_dispatch: Dispatch<MainState>, id: Uuid) -> Option<Plate
.iter() .iter()
.any(|x| x.get_uuid() == id) .any(|x| x.get_uuid() == id)
{ {
Some(PlateType::Source) return Some(PlateType::Source);
} else if main_dispatch } else if main_dispatch
.get() .get()
.destination_plates .destination_plates

View File

@ -2,7 +2,7 @@
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::new_plate_dialog::{NewPlateDialog, NewPlateDialogType}; use crate::components::new_plate_dialog::NewPlateDialog;
use crate::components::plates::plate_container::PlateContainer; use crate::components::plates::plate_container::PlateContainer;
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::states::{CurrentTransfer, MainState};
use crate::components::transfer_menu::TransferMenu; use crate::components::transfer_menu::TransferMenu;
@ -53,7 +53,7 @@ pub fn MainWindow() -> Html {
main_window_callbacks::change_csv_export_type_callback(main_dispatch) main_window_callbacks::change_csv_export_type_callback(main_dispatch)
}; };
let new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>> = use_state_eq(|| None); let new_plate_dialog_is_open = use_state_eq(|| false);
let new_plate_dialog_callback = let new_plate_dialog_callback =
main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone()); main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone());
@ -124,8 +124,8 @@ pub fn MainWindow() -> Html {
<TransferMenu /> <TransferMenu />
<PlateContainer source_dims={source_plate_instance} <PlateContainer source_dims={source_plate_instance}
destination_dims={destination_plate_instance}/> destination_dims={destination_plate_instance}/>
if {new_plate_dialog_is_open.is_some()} { if {*new_plate_dialog_is_open} {
<NewPlateDialog close_callback={new_plate_dialog_callback} dialog_type={new_plate_dialog_is_open.unwrap()}/> <NewPlateDialog close_callback={new_plate_dialog_callback}/>
} }
</div> </div>
</> </>

View File

@ -10,16 +10,6 @@ use crate::components::callbacks::new_plate_dialog_callbacks;
#[derive(PartialEq, Properties)] #[derive(PartialEq, Properties)]
pub struct NewPlateDialogProps { pub struct NewPlateDialogProps {
pub close_callback: Callback<()>, pub close_callback: Callback<()>,
pub dialog_type: NewPlateDialogType,
}
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum NewPlateDialogType {
SourceOnly,
DestinationOnly,
#[allow(dead_code)]
Both, // Retained old functionality
} }
#[function_component] #[function_component]
@ -53,15 +43,9 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
); );
} }
let header_interior_text = match props.dialog_type {
NewPlateDialogType::Both => "",
NewPlateDialogType::SourceOnly => " source",
NewPlateDialogType::DestinationOnly => " destination",
};
html! { html! {
<dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}> <dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}>
<h2>{format!("Create a{} plate:", header_interior_text)}</h2> <h2>{"Create a plate:"}</h2>
<form onsubmit={new_plate_callback}> <form onsubmit={new_plate_callback}>
<input type="text" name="new_plate_name" placeholder="Name"/> <input type="text" name="new_plate_name" placeholder="Name"/>
<select name="plate_format"> <select name="plate_format">
@ -74,35 +58,13 @@ pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
<option value="1536">{"1536"}</option> <option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option> <option value="3456">{"3456"}</option>
</select> </select>
{ plate_type_selector(props.dialog_type) } <input type="radio" name="new_plate_type" id="npt_src" value="src" />
<label for="npt_src">{"Source"}</label>
<input type="radio" name="new_plate_type" id="npt_dest" value="dest" />
<label for="npt_dest">{"Destination"}</label>
<input type="submit" name="new_plate_button" value="Create" /> <input type="submit" name="new_plate_button" value="Create" />
</form> </form>
<form class="modal_close" method="dialog"><button /></form> <form class="modal_close" method="dialog"><button /></form>
</dialog> </dialog>
} }
} }
fn plate_type_selector(t: NewPlateDialogType) -> Html {
match t {
NewPlateDialogType::Both => {
html! {
<>
<input type="radio" name="new_plate_type" id="npt_src" value="src" />
<label for="npt_src">{"Source"}</label>
<input type="radio" name="new_plate_type" id="npt_dest" value="dest" />
<label for="npt_dest">{"Destination"}</label>
</>
}
},
NewPlateDialogType::SourceOnly => {
html! {
<input type="hidden" name="new_plate_type" id="npt_src" value="src" />
}
},
NewPlateDialogType::DestinationOnly => {
html! {
<input type="hidden" name="new_plate_type" id="npt_dest" value="dest" />
}
}
}
}

View File

@ -1,9 +1,6 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use std::collections::HashMap; use std::collections::HashMap;
use plate_tool_lib::transfer_volume::TransferVolume;
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
@ -11,7 +8,6 @@ use crate::components::states::{CurrentTransfer, MainState};
use plate_tool_lib::plate::PlateType; use plate_tool_lib::plate::PlateType;
use plate_tool_lib::transfer::Transfer; use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::Region; use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::Well;
// Color Palette for the Source Plates, can be changed here // Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes; use crate::components::plates::util::Palettes;
@ -36,24 +32,24 @@ pub fn Plate(props: &PlateProps) -> Html {
PlateType::Destination => ct_state.transfer.transfer_region.dest_region.clone(), PlateType::Destination => ct_state.transfer.transfer_region.dest_region.clone(),
}; };
let (pt1, pt2) = match region { let (pt1, pt2) = match region {
Region::Point(Well {row: x, col: y }) => ((x, y), (x, y)), Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => ((c1.row, c1.col ), (c2.row, c2.col)), Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0, 0), (0, 0)), Region::Custom(_) => ((0, 0), (0, 0)),
}; };
m_start_handle.set(Some(pt1)); m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2)); m_end_handle.set(Some(pt2));
} }
let tooltip_map: HashMap<Well, Vec<&Transfer>>; let tooltip_map: HashMap<(u8, u8), Vec<&Transfer>>;
let volume_map: HashMap<Well, f32>; let volume_map: HashMap<(u8, u8), f32>;
let volume_max: f32; let volume_max: f32;
{ {
let transfers = main_state.transfers.iter().filter(|t| match props.ptype { let transfers = main_state.transfers.iter().filter(|t| match props.ptype {
PlateType::Source => t.source_id == props.source_plate.get_uuid(), PlateType::Source => t.source_id == props.source_plate.get_uuid(),
PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(), PlateType::Destination => t.dest_id == props.destination_plate.get_uuid(),
}); });
let mut tooltip_map_temp: HashMap<Well, Vec<&Transfer>> = HashMap::new(); let mut tooltip_map_temp: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new();
let mut volume_map_temp: HashMap<Well, f32> = HashMap::new(); let mut volume_map_temp: HashMap<(u8,u8), f32> = HashMap::new();
let mut volume_max_temp: f32 = f32::NEG_INFINITY; let mut volume_max_temp: f32 = f32::NEG_INFINITY;
for transfer in transfers { for transfer in transfers {
let wells = match props.ptype { let wells = match props.ptype {
@ -66,27 +62,12 @@ pub fn Plate(props: &PlateProps) -> Html {
} else { } else {
tooltip_map_temp.insert(well, vec![transfer]); tooltip_map_temp.insert(well, vec![transfer]);
} }
let temp_volume: f32 = match &transfer.volume {
TransferVolume::Single(x) => *x,
TransferVolume::WellMap(wm) => {
*match props.ptype {
PlateType::Source => wm.source_only.get(&well),
PlateType::Destination => wm.destination_only.get(&well)
}.unwrap_or(&2.5f32)
},
_ => unreachable!(),
};
// Usage to be used as a volume multiplier; should be in [1, U32_MAX]
// First convert to f64 (u32 can not fit in f32 directly) and then cast
// to round into the closest f32.
let usage: f32 = Into::<f64>::into(count_plate_usage(transfer, &well).unwrap_or(1u32).max(1u32)) as f32;
if let Some(val) = volume_map_temp.get_mut(&well) { if let Some(val) = volume_map_temp.get_mut(&well) {
*val += temp_volume * usage; *val += transfer.volume;
} else { } else {
volume_map_temp.insert(well, temp_volume * usage); volume_map_temp.insert(well, transfer.volume);
} }
volume_max_temp = f32::max(volume_max_temp, *volume_map_temp.get(&well).expect("Just added")); volume_max_temp = f32::max(volume_max_temp, transfer.volume);
} }
} }
tooltip_map = tooltip_map_temp; tooltip_map = tooltip_map_temp;
@ -154,22 +135,22 @@ pub fn Plate(props: &PlateProps) -> Html {
.map(|j| { .map(|j| {
let color = { let color = {
if !main_state.preferences.volume_heatmap { if !main_state.preferences.volume_heatmap {
tooltip_map.get(&Well { row: i, col: j }) tooltip_map.get(&(i,j))
.and_then(|t| t.last()) .and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids)) .map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
} else { } else {
volume_map.get(&Well { row: i, col: j }) volume_map.get(&(i,j))
.map(|t| PALETTE.get_linear(*t as f64, volume_max as f64)) .map(|t| PALETTE.get_linear(*t as f64, volume_max as f64))
} }
}; };
let title = { let title = {
let mut out = String::new(); let mut out = String::new();
let used_by = tooltip_map.get(&Well { row: i, col: j }).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone()) let used_by = tooltip_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", "))); .collect::<Vec<_>>().join(", ")));
if let Some(val) = used_by { if let Some(val) = used_by {
out += &val; out += &val;
} }
let volume_sum = volume_map.get(&Well { row: i, col: j }) let volume_sum = volume_map.get(&(i,j))
.map(|t| format!("Volume: {}", t)); .map(|t| format!("Volume: {}", t));
if let Some(val) = volume_sum { if let Some(val) = volume_sum {
if !out.is_empty() { out += "\n" } if !out.is_empty() { out += "\n" }
@ -181,7 +162,7 @@ pub fn Plate(props: &PlateProps) -> Html {
<PlateCell i={i} j={j} <PlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))} selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()} mouse={mouse_callback.clone()}
in_transfer={wells.contains(&Well { row: i, col: j }) && main_state.preferences.in_transfer_hashes} in_transfer={wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={ color } color={ color }
cell_height={props.cell_height} cell_height={props.cell_height}
title={title} title={title}
@ -275,15 +256,6 @@ pub fn in_rect(corner1: Option<(u8, u8)>, corner2: Option<(u8, u8)>, pt: (u8, u8
} }
} }
fn count_plate_usage(transfer: &Transfer, well: &Well) -> Option<u32> {
if let Region::Custom(_) = transfer.transfer_region.source_region {
return None;
}
let map = transfer.transfer_region.calculate_map();
map(*well).map(|list| list.len() as u32)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use wasm_bindgen_test::*; use wasm_bindgen_test::*;

View File

@ -41,7 +41,7 @@ pub fn mouseup_callback(
m_stat_handle.set(false); m_stat_handle.set(false);
if let Some(ul) = *m_start_handle { if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle { if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.1, ul.0, br.1, br.0)) { if let Ok(rd) = RegionDisplay::try_from((ul.0, ul.1, br.0, br.1)) {
ct_dispatch.reduce_mut(|state| { ct_dispatch.reduce_mut(|state| {
match ptype { match ptype {
PlateType::Source => state.transfer.transfer_region.source_region = Region::from(&rd), PlateType::Source => state.transfer.transfer_region.source_region = Region::from(&rd),

View File

@ -5,12 +5,10 @@ use yew::prelude::*;
use plate_tool_lib::plate::PlateType; use plate_tool_lib::plate::PlateType;
use plate_tool_lib::plate_instances::PlateInstance; use plate_tool_lib::plate_instances::PlateInstance;
use yewdux::functional::use_store;
// use super::destination_plate::DestinationPlate; // use super::destination_plate::DestinationPlate;
// use super::source_plate::SourcePlate; // use super::source_plate::SourcePlate;
use super::plate::Plate; use super::plate::Plate;
use crate::components::states::MainState;
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct PlateContainerProps { pub struct PlateContainerProps {
@ -20,8 +18,6 @@ pub struct PlateContainerProps {
#[function_component] #[function_component]
pub fn PlateContainer(props: &PlateContainerProps) -> Html { pub fn PlateContainer(props: &PlateContainerProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let cell_height = { let cell_height = {
let height = web_sys::window() let height = web_sys::window()
.unwrap() .unwrap()
@ -53,15 +49,8 @@ pub fn PlateContainer(props: &PlateContainerProps) -> Html {
.set_onresize(Some(onresize.as_ref().unchecked_ref())); .set_onresize(Some(onresize.as_ref().unchecked_ref()));
onresize.forget(); // Magic! onresize.forget(); // Magic!
let heatmap_enabled = main_state.preferences.volume_heatmap;
html! { html! {
<div class="plate_container"> <div class="plate_container">
if heatmap_enabled {
<div class="plate_container--heatmap-notice" >
<h3>{"Volume Heatmap Enabled"}</h3>
</div>
}
if let Some(spi) = props.source_dims.clone() { if let Some(spi) = props.source_dims.clone() {
if let Some(dpi) = props.destination_dims.clone() { if let Some(dpi) = props.destination_dims.clone() {
<div class="plate_container--source"> <div class="plate_container--source">

View File

@ -26,7 +26,6 @@ impl ColorPalette {
] ]
} }
#[allow(dead_code)] // Preserve old implementation for reference
fn get_u8(&self, t: u8) -> [f64; 3] { fn get_u8(&self, t: u8) -> [f64; 3] {
assert!(t > 0, "t must be greater than zero!"); assert!(t > 0, "t must be greater than zero!");
self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64) self.get((2f64.powi(-(t.ilog2() as i32))) * (t as f64 + 0.5f64) - 1.0f64)

View File

@ -1,7 +1,6 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use lazy_static::lazy_static; use lazy_static::lazy_static;
use plate_tool_lib::Well;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use yew::prelude::*; use yew::prelude::*;
@ -9,7 +8,6 @@ use yewdux::prelude::*;
use crate::components::callbacks::transfer_menu_callbacks; use crate::components::callbacks::transfer_menu_callbacks;
use plate_tool_lib::transfer_region::Region; use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::transfer_volume::TransferVolume;
use plate_tool_lib::util::{letters_to_num, num_to_letters}; use plate_tool_lib::util::{letters_to_num, num_to_letters};
use super::states::{CurrentTransfer, MainState}; use super::states::{CurrentTransfer, MainState};
@ -145,10 +143,7 @@ pub fn TransferMenu() -> Html {
<input type="number" name="volume" class="volume_input" <input type="number" name="volume" class="volume_input"
min="0" step="0.1" min="0" step="0.1"
onchange={on_volume_change} onchange={on_volume_change}
value={match ct_state.transfer.volume { value={ct_state.transfer.volume.to_string()}/>
TransferVolume::Single(x) => x.to_string(),
_ => unreachable!(),
}}/>
</div> </div>
} }
<div id="controls"> <div id="controls">
@ -238,10 +233,10 @@ impl TryFrom<&str> for RegionDisplay {
impl From<&Region> for RegionDisplay { impl From<&Region> for RegionDisplay {
fn from(value: &Region) -> Self { fn from(value: &Region) -> Self {
match *value { match *value {
Region::Point(Well { row, col }) => { Region::Point((col, row)) => {
RegionDisplay::try_from((col, row, col, row)).ok().unwrap() RegionDisplay::try_from((col, row, col, row)).ok().unwrap()
} }
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.row, c1.col, c2.row, c2.col)) Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1))
.ok() .ok()
.unwrap(), .unwrap(),
Region::Custom(_) => RegionDisplay { Region::Custom(_) => RegionDisplay {
@ -257,11 +252,11 @@ impl From<&Region> for RegionDisplay {
impl From<&RegionDisplay> for Region { impl From<&RegionDisplay> for Region {
fn from(value: &RegionDisplay) -> Self { fn from(value: &RegionDisplay) -> Self {
if value.col_start == value.col_end && value.row_start == value.row_end { if value.col_start == value.col_end && value.row_start == value.row_end {
Region::Point(Well { row: value.row_start, col: value.col_start }) Region::Point((value.col_start, value.row_start))
} else { } else {
Region::Rect( Region::Rect(
Well { row: value.row_start, col: value.col_start }, (value.col_start, value.row_start),
Well { row: value.row_end, col: value.col_end }, (value.col_end, value.row_end),
) )
} }
} }

View File

@ -2,18 +2,17 @@
use plate_tool_lib::plate::PlateFormat; use plate_tool_lib::plate::PlateFormat;
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{HtmlDialogElement}; use web_sys::{HtmlDialogElement, HtmlInputElement};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
use crate::components::callbacks::{tree_callbacks}; use crate::components::callbacks::tree_callbacks;
use crate::components::new_plate_dialog::NewPlateDialogType;
use crate::components::states::{CurrentTransfer, MainState}; use crate::components::states::{CurrentTransfer, MainState};
#[derive(PartialEq, Properties)] #[derive(PartialEq, Properties)]
pub struct TreeProps { pub struct TreeProps {
pub open_new_plate_callback: Callback<NewPlateDialogType>, pub open_new_plate_callback: Callback<()>,
} }
#[function_component] #[function_component]
@ -99,25 +98,13 @@ pub fn Tree(props: &TreeProps) -> Html {
html! { html! {
<div class="tree"> <div class="tree">
<div id="source-plates"> <div id="source-plates">
<div class="tree--header">
<h3>{"Source Plates:"}</h3> <h3>{"Source Plates:"}</h3>
<button onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(NewPlateDialogType::SourceOnly)}
} />
</div>
<ul> <ul>
{source_plates} {source_plates}
</ul> </ul>
</div> </div>
<div id="destination-plates"> <div id="destination-plates">
<div class="tree--header">
<h3>{"Destination Plates:"}</h3> <h3>{"Destination Plates:"}</h3>
<button onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(NewPlateDialogType::DestinationOnly)}
} />
</div>
<ul> <ul>
{dest_plates} {dest_plates}
</ul> </ul>
@ -133,6 +120,14 @@ pub fn Tree(props: &TreeProps) -> Html {
delete_button_callback={plate_info_delete_callback}/> delete_button_callback={plate_info_delete_callback}/>
} }
<div id="controls">
<button type="button"
onclick={
let open_new_plate_callback = props.open_new_plate_callback.clone();
move |_| {open_new_plate_callback.emit(())}
}>
{"New Plate"}</button>
</div>
</div> </div>
} }
} }
@ -164,14 +159,16 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
None => "Not Found".to_string(), None => "Not Found".to_string(),
}; };
let plate_format = plate.map(|p| p.plate.plate_format); let plate_format = plate.map(|p| p.plate.plate_format);
let plate_formats = [PlateFormat::W6, let plate_formats = vec![
PlateFormat::W6,
PlateFormat::W12, PlateFormat::W12,
PlateFormat::W24, PlateFormat::W24,
PlateFormat::W48, PlateFormat::W48,
PlateFormat::W96, PlateFormat::W96,
PlateFormat::W384, PlateFormat::W384,
PlateFormat::W1536, PlateFormat::W1536,
PlateFormat::W3456]; PlateFormat::W3456,
];
let plate_format_options = plate_formats.iter().map(|v| { let plate_format_options = plate_formats.iter().map(|v| {
let selected = Some(v) == plate_format.as_ref(); let selected = Some(v) == plate_format.as_ref();
html!{ html!{

View File

@ -25,9 +25,9 @@ pub fn plate_test() {
let transfer = transfer_region::TransferRegion { let transfer = transfer_region::TransferRegion {
source_plate: source, source_plate: source,
source_region: transfer_region::Region::Rect(Well { row: 1, col: 1 }, Well { row: 2, col: 2 }), source_region: transfer_region::Region::Rect((1, 1), (2, 2)),
dest_plate: destination, dest_plate: destination,
dest_region: transfer_region::Region::Rect(Well { row: 2, col: 2 }, Well { row: 11, col: 11 }), dest_region: transfer_region::Region::Rect((2, 2), (11, 11)),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (3, 3), interleave_dest: (3, 3),
}; };