Compare commits

...

76 Commits

Author SHA1 Message Date
Emilia Allison e8511c01b0
fix: Update README ToC
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-11 13:56:14 -04:00
Emilia Allison 83d74cfc6f
fix: Compiler lints
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-11 11:55:46 -04:00
Emilia Allison 9aad876a31
fix: More broken tests 2024-08-11 11:51:19 -04:00
Emilia Allison b25d1eb0c6
clippy 2024-08-11 11:47:53 -04:00
Emilia Allison c6d3b86237
fix: Tests had broken references 2024-08-11 11:47:34 -04:00
Emilia Allison 51093f5efd
fix: Consider well re-usage when calculating volume
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-10 04:52:23 -04:00
Emilia Allison a1d4cc74c5
Bump versions and update README
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-10 04:31:24 -04:00
Emilia Allison e88fdb0cdd
feature: Volume management for CSV import
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
Also more likely to be correct elsewhere
2024-08-10 04:15:30 -04:00
Emilia Allison 7771ce1786
refactor: Well struct
most devious refactoring
2024-08-10 02:39:37 -04:00
Emilia Allison ca142ef594
Revert "refactor: Well struct"
This reverts commit 7e321a78c4.
2024-08-10 01:40:25 -04:00
Emilia Allison deac725fdb
Revert "refactor: Well struct in web"
This reverts commit 63790c2145.
2024-08-10 01:40:14 -04:00
Emilia Allison 7031ae33dc
fix: SCSS selector 2024-08-10 01:01:34 -04:00
Emilia Allison 63790c2145
refactor: Well struct in web 2024-08-10 00:58:55 -04:00
Emilia Allison 7e321a78c4
refactor: Well struct
Will require bump to lib version!!
2024-08-10 00:52:50 -04:00
Emilia Allison 06baf0a053
feature: Heatmap enabled indicator 2024-08-09 22:57:50 -04:00
Emilia Allison 52b28aabb1
fix: Button styling updates 2024-08-09 22:35:00 -04:00
Emilia Allison 9e1abdc8bb
fix: Expand transfer name width 2024-08-09 21:07:59 -04:00
Emilia Allison e829a49424
fix: Volume not saved on new transfer creation 2024-08-09 21:01:50 -04:00
Emilia Allison 2ccb84041b
fix: CSS for plate add buttons
Some improvement, still not great
2024-08-09 20:58:27 -04:00
Emilia Allison caf10f10c1
feature: Separate add plate button per type 2024-08-09 20:33:09 -04:00
Emilia Allison 98d2d92b49
Update documentation 2024-08-09 19:39:41 -04:00
Emilia Allison ce6e88a347
Add some doc comments to lib
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-04 14:48:45 -04:00
Emilia Allison 94386a0485
fix: Change CSV serialization headers
Makes for easier imports into Cellario CP orders
shoutout to my employer ;)
2024-08-04 14:01:26 -04:00
Emilia Allison 7defd8ff08
Bump versions
temp/pipeline/head Something is wrong with the build of this commit Details
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-08-03 18:49:57 -04:00
Emilia Allison be1ededc7e
fix: Styling bugs
- Drop down items not wide enough
- Dialog close button intersected modal text
2024-08-03 18:49:49 -04:00
Emilia Allison 0567e0a037
feature: Add CSV export type to plate-tool-web 2024-08-03 18:42:31 -04:00
Emilia Allison b78336def0
Clean export callbacks 2024-08-03 18:42:00 -04:00
Emilia Allison 194b78430c
Add Echo Client format support in lib 2024-08-03 17:55:13 -04:00
Emilia Allison 3fcd526010
fix: Remove debug call
This one call was spamming my logs and making them unreadable
2024-08-03 17:39:34 -04:00
Emilia Allison 108a2677e3
fix: Retain unknown columns when mangling headers on CSV import 2024-08-03 17:37:41 -04:00
Emilia Allison ad57482dea
Bump rev version for web
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-21 10:05:40 -05:00
Emilia Allison 12684c0eea
Merge plates
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
i wrote this so late it probably does not work ughhhhhhhhh
2024-02-20 23:31:56 -05:00
Emilia Allison 0ec29f6783
Reorganize tree_callbacks
I am going to make too many
2024-02-20 21:26:24 -05:00
Emilia Allison e01468b63a
Reorganize main_window_callbacks
There were so many
2024-02-20 21:18:53 -05:00
Emilia Allison 8f82c4e224
Linear volume heatmap visualization
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-18 22:01:08 -05:00
Emilia Allison 2fdc15c9aa
Accept higher version of lib on web 2024-02-18 22:00:41 -05:00
Emilia Allison f7f492b70e
feature: Improved CSV Parsing
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
1. Superior field detection: short of having actual typos, most ways to
   express a field should now be properly registered. Further, it will
   be considerably easier to add new variants.
2. Numeric well parsing: some systems do not use alphanumeric wells like
   H12 or E7. Purely numeric wells will now be supported but only if the
   plate format is manually specified; this feels like a good tradeoff
   since a failed detection would yield very odd behaviour to a user.
2024-02-16 20:57:06 -05:00
Emilia Allison e546fa354e
Merge branch 'main' into beta-release
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-13 22:21:31 -05:00
Emilia Allison 3962fef9b1
add license
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
note: before the addition of this license, technically
plate-tool was owned _in full_ by me!
also note: plate-tool was being developed prior to my
employment anywhere.
2024-02-13 22:18:43 -05:00
Emilia Allison 6cfa686b55
fix: not switching on plate type
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
me when i migrate correctly the first time and have no lingering bugs
2024-02-13 22:06:55 -05:00
Emilia Allison 72d81439c1
fix: custom get_destination_wells
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
The code here was like, stupid tbh.
2024-02-13 21:54:23 -05:00
Emilia Allison 94cb530de8
Remove log line 2024-02-13 21:10:31 -05:00
Emilia Allison c3995f5725
Bump version numbers
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-13 20:57:53 -05:00
Emilia Allison 7556f528a6
update readme 2024-02-13 20:53:57 -05:00
Emilia Allison 6dc13675ae
hotfix: Jenkinsfile 2
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-13 20:48:54 -05:00
Emilia Allison 2a463e2133
hotfix: Jenkinsfile
Gitea Scan/plate-tool/pipeline/head There was a failure building this commit Details
2024-02-13 20:46:17 -05:00
Emilia Allison e71a72bff9
Merge branch 'dev' into beta-release
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-13 20:44:29 -05:00
Emilia Allison 430d0aa845
Merge branch 'main' into beta-release
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-13 20:43:18 -05:00
Emilia Allison 9366d29202
lib: fix stupid typo 2024-02-13 20:32:42 -05:00
Emilia Allison c2a64d679a
web: auto button 2024-02-13 20:27:58 -05:00
Emilia Allison 1aa4c1b7bb
web: use new utility function in callback 2024-02-13 19:58:14 -05:00
Emilia Allison 6c8533f7a6
lib: auto creation from csv 2024-02-13 19:56:47 -05:00
Emilia Allison d7c98d37a2
Reorg csv in lib 2024-02-13 18:33:42 -05:00
Emilia Allison bfa1fef9d8
update readme 2024-02-13 18:23:22 -05:00
Emilia Allison b79c377793
Change plate format 2024-02-11 22:04:03 -05:00
Emilia Allison 9db0d4aa19
lib migration pt4 2024-02-11 20:49:52 -05:00
Emilia Allison 4d82ea5567
lib migration pt3 2024-02-11 20:03:29 -05:00
Emilia Allison 7d35959ac2
lib migration pt 2 2024-02-11 19:46:43 -05:00
Emilia Allison ffc81f505b
lib migration pt1 2024-02-11 19:32:30 -05:00
Emilia Allison a8b72c0a7a
huge clippy 2024-02-10 22:00:39 -05:00
Emilia Allison 338a7b98c7
the refactor i've wanted most
SourcePlate and DestinationPlate are now the same
why did i not do this originally omg
2024-02-10 21:57:30 -05:00
Emilia Allison 0625854895
More robust CSV parsing
Permit lowercases, alternative column names
2024-02-10 18:00:47 -05:00
Emilia Allison 8a3bbc8b92
Refactor import_csv callbacks 2024-02-10 17:25:04 -05:00
Emilia Allison de42499444
Close buttons on imports
Resolves #3
2024-02-10 17:18:28 -05:00
Emilia Allison e836c4264a
Hotfix merge
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-10 16:49:56 -05:00
Emilia Allison 5430b3d42c
hotfix
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
temp/pipeline/head There was a failure building this commit Details
2024-02-10 16:47:32 -05:00
Emilia Allison 020f7740d3
Add callbacks per #2
Gitea Scan/plate-tool/pipeline/head There was a failure building this commit Details
Squashed commit of the following:

commit 4710335750
Author: Emilia <contact@emiliaallison.com>
Date:   Sat Feb 10 16:34:52 2024 -0500

    tree callbacks

commit f8216cb0bd
Author: Emilia <contact@emiliaallison.com>
Date:   Sat Feb 10 09:20:59 2024 -0500

    transfer_menu callbacks

commit 15accc2fca
Author: Emilia <contact@emiliaallison.com>
Date:   Tue Jan 30 20:37:15 2024 -0500

    I think this file was supposed to be in the last one lol

commit 53457a3e86
Author: Emilia <contact@emiliaallison.com>
Date:   Sat Jan 13 14:05:34 2024 -0500

    new_plate_dialog callbacks

    For #2

commit c2a3f0302b
Author: Emilia <contact@emiliaallison.com>
Date:   Sat Jan 13 13:57:14 2024 -0500

    Finish main_window callback refactor

    For #2

commit 820f672cb7
Author: Emilia <contact@emiliaallison.com>
Date:   Sat Jan 13 13:23:11 2024 -0500

    Import json button refactor

    Partial for #2

commit a90b5f83d8
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Jan 12 22:22:28 2024 -0500

    A number of moves to address #2

commit 62d870521e
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Jan 12 21:44:10 2024 -0500

    First callback move

commit e2ef9fa84d
Author: Emilia <contact@emiliaallison.com>
Date:   Fri Jan 12 21:43:50 2024 -0500

    Increment version number in cargo.lock

    why wasn't this done already?
2024-02-10 16:38:00 -05:00
Emilia Allison 4710335750
tree callbacks
Gitea Scan/plate-tool/pipeline/head There was a failure building this commit Details
2024-02-10 16:34:52 -05:00
Emilia Allison f8216cb0bd
transfer_menu callbacks
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-02-10 09:20:59 -05:00
Emilia Allison 15accc2fca
I think this file was supposed to be in the last one lol 2024-01-30 20:37:15 -05:00
Emilia Allison 53457a3e86
new_plate_dialog callbacks
Gitea Scan/plate-tool/pipeline/head There was a failure building this commit Details
For #2
2024-01-13 14:05:34 -05:00
Emilia Allison c2a3f0302b
Finish main_window callback refactor
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
For #2
2024-01-13 13:57:14 -05:00
Emilia Allison 820f672cb7
Import json button refactor
Partial for #2
2024-01-13 13:23:11 -05:00
Emilia Allison a90b5f83d8
A number of moves to address #2
Gitea Scan/plate-tool/pipeline/head This commit looks good Details
2024-01-12 22:22:28 -05:00
Emilia Allison 62d870521e
First callback move
Gitea Scan/plate-tool/pipeline/head There was a failure building this commit Details
2024-01-12 21:44:10 -05:00
Emilia Allison e2ef9fa84d
Increment version number in cargo.lock
why wasn't this done already?
2024-01-12 21:43:50 -05:00
78 changed files with 4655 additions and 2435 deletions

687
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,3 @@
[package] [workspace]
name = "plate-tool" members = ["plate-tool-web", "plate-tool-lib"]
version = "0.2.0" resolver = "2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
yew = { version = "0.20.0", features = ["csr"] }
yewdux = "0.9"
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement",
"HtmlDialogElement", "Blob", "Url", "Window",
"HtmlAnchorElement", "ReadableStream", "HtmlSelectElement", "HtmlOptionElement", "HtmlButtonElement",
"FileReader"] }
js-sys = "0.3"
log = "0.4"
wasm-logger = "0.2"
regex = "1"
lazy_static = "1.4"
uuid = { version = "1.6", features = ["v7", "fast-rng", "macro-diagnostics", "js", "serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.2"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

5
Jenkinsfile vendored
View File

@ -12,19 +12,20 @@ pipeline {
steps { steps {
sh ''' sh '''
. "$HOME/.cargo/env" . "$HOME/.cargo/env"
cd plate-tool-web
trunk build --release --public-url "cool-stuff/$OUTPUT_DIR/" trunk build --release --public-url "cool-stuff/$OUTPUT_DIR/"
''' '''
} }
} }
stage('Archive') { stage('Archive') {
steps { steps {
zip zipFile: "dist.zip", archive: true, dir: "dist/" zip zipFile: "dist.zip", archive: true, dir: "plate-tool-web/dist/"
archiveArtifacts artifacts: "dist.zip", fingerprint: true archiveArtifacts artifacts: "dist.zip", fingerprint: true
} }
} }
stage('Transfer') { stage('Transfer') {
steps { steps {
sh 'echo "put -r dist/" | sftp oracle' sh 'echo "put -r plate-tool-web/dist/" | sftp oracle'
} }
} }
stage('Deploy') { stage('Deploy') {

249
README.md
View File

@ -1,136 +1,193 @@
# plate-tool # plate-tool
A web-based tool for creating assays for your favorite (acoustic) liquid handler. A web-based tool for creating and visualizing picklists for your favorite (possibly acoustic) liquid handler.
## Table of Contents ## Table of Contents
- [Usage](#Usage) - [Usage](#usage)
- [Installation](#Installation) - [Plates](#plates)
- [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
### Adding plates ### 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 "New Plate" button: To add a new plate, click the "Add" button for the corresponding plate type:
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 and deleting plates #### Modifying 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
(note that this will not delete any data if you accidentally switch to a smaller format).
### Adding a transfer ### Transfers
Now that you have two plates selected, #### Adding a transfer
it's time to add a transfer. Now that you have two plates selected,
We can see all of the properties of our transfer in the bottom-left pane. it's time to add a transfer.
You should first name your transfer (this name is only used for your reference, and is not passed to the liquid handler). We can see all of the properties of our transfer in the bottom-left pane.
You can enter your source and destination regions in their respective fields; You should first name your transfer (this name is only used for your reference, and is not passed to the liquid handler).
the accepted format should be familiar—capital letters for the row and arabic numerals for the column. You can enter your source and destination regions in their respective fields;
the accepted format should be familiar—capital letters for the row and arabic numerals for the column.
However, it is much easier to click-and-drag the desired region. However, it is much easier to click-and-drag the desired region.
If we click and hold on a well (see right pane), that specifies our start well. If we click and hold on a well (see right pane), that specifies our start well.
Then, we can drag and subsequently release on our desired end well. Then, we can drag and subsequently release on our desired end well.
Our selected wells will be highlighted in light blue for our source plate and light red for our destination plate. Our selected wells will be highlighted in light blue for our source plate and light red for our destination plate.
You might also notice that some wells are hatched: You might also notice that some wells are hatched:
this indicates wells that will be used in the transfer. this indicates wells that will be used in the transfer.
Not all selected wells will necessarily be hatched, Not all selected wells will necessarily be hatched,
depending on the transfer type and interleave settings. 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 and deleting transfers #### Modifying 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.
When finished, click the "Save" button to commit these changes. When finished, click the "Save" button to commit these changes.
If you no longer need a transfer, select it as above and then click the "Delete" button. If you no longer need a transfer, select it as above and then click the "Delete" button.
### Importing and Exporting ### Importing and Exporting
#### Export as CSV #### Export as CSV
Exporting the transfers we have created to a CSV format is the primary (if not sole) usage of Plate Tool. Exporting the transfers we have created to a CSV format is the primary (if not sole) usage of Plate Tool.
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 reminded that this is a one-way export (see JSON export/import below), You will be prompted by your browser to select a location for your file.
and then prompted by your browser to select a location for your file.
#### Export as JSON (Saving Your Work) As of version `0.4.0`, it is possible to pick a CSV export format:
Currently, it is not possible to export to a format produced by other similar software. Mouse over options, then export, then click "Change CSV export type".
However, you might reasonably want to save a copy of your work In the dialog that opens, select your desired export type.
either as a backup or to share. Currently, plate-tool supports:
Mouse over the "File" tab, then "Export" as above, then alternatively select "Export as JSON". - Normal
Your browser will then prompt you to pick a suitable location to save your work as a file. - This format can be imported by Cellario's cherrypick hook.
(See note 1 below) - 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.
#### Import from JSON (Recovering Your Work) #### Export as JSON
If we want to import one such file, mouse over the "File" tab as before ##### (Saving Your Work)
and select "Import", and finally click "Import from JSON". Currently, it is not possible to export to a format produced by other similar software.
This opens a modal where you are prompted to upload (see note 2) However, you might reasonably want to save a copy of your work
your file; it will then be processed and loaded. either as a backup or to share.
Keep in mind that this will overwrite any work you currently have open, Mouse over the "File" tab, then "Export" as above, then alternatively select "Export as JSON".
so you may wish to export first (see above). Your browser will then prompt you to pick a suitable location to save your work as a file.
(See note 1 below)
#### Import Transfer from CSV (Using a picklist as a transfer) #### Import from JSON
If you have a CSV generated by another tool (or plate-tool), ##### (Recovering Your Work)
you can import it as a single transfer. If we want to import one such file, mouse over the "File" tab as before
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV". and select "Import", and finally click "Import from JSON".
When creating transfers via this method, the transfer cannot be edited. This opens a modal where you are prompted to upload (see note 2)
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool. your file; it will then be processed and loaded.
Keep in mind that this will overwrite any work you currently have open,
so you may wish to export first (see above).
_Note 1_: JSON files are plaintext! _Note 1_: JSON files are plaintext!
By default there is little whitespace (this makes comprehending them a challenge) By default there is little whitespace (this makes comprehending them a challenge)
but if we pass it through a "JSON Beautifier" (enter this into your search engine of choice) but if we pass it through a "JSON Beautifier" (enter this into your search engine of choice)
it immediately becomes more readable. it immediately becomes more readable.
It is encouraged (although by no means necessary) to take a look at your export; It is encouraged (although by no means necessary) to take a look at your export;
you will see that the representation here very closely mirrors the representation presented you will see that the representation here very closely mirrors the representation presented
in Plate Tool. in Plate Tool.
_Note 2_: Use of the word "upload" might imply that your data is leaving your computer. _Note 2_: Use of the word "upload" might imply that your data is leaving your computer.
It does not. It does not.
You are welcome to verify (use your browser's developer tools, it should have a network tab) You are welcome to verify (use your browser's developer tools, it should have a network tab)
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
##### (Using a picklist as a transfer)
If you have a CSV generated by another tool (or plate-tool),
you can import it as a single transfer.
To do so, mouse over the "File" tab, then "Import", and finally "Import Transfer from CSV".
When creating transfers via this method, the transfer cannot be edited.
This is useful if you have a pre-existing picklist that you would like to visualize in plate-tool.
You may either manually map plates in a picklist to plates you've already created, or click "Auto" to:
1. Generate all plates in the picklist
2. Generate transfers for all source:destination pairs.
_Note_: If you try to use this feature and no plates are available to select,
there was likely an issue parsing your picklist.
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.
_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
If you double click on a plate If you double click on a plate
(try to avoid clicking a well since that will change your selection) (try to avoid clicking a well since that will change your selection)
plate-tool will do some magic to take a screenshot of your plate 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
in a "secure context" (localhost or https). in a "secure context" (localhost or https).
#### Turn off the in-transfer hashes #### Turn off the in-transfer hashes
Have you noticed that when you select a transfer, the wells to Have you noticed that when you select a transfer, the wells to
be used in that transfer have little diagonal lines over them? be used in that transfer have little diagonal lines over them?
Hopefully you have, because this is supposed to happen! Hopefully you have, because this is supposed to happen!
However, if you want to take a Pretty Plate Picture (see above), However, if you want to take a Pretty Plate Picture (see above),
you might want these indicators turned off. you might want these indicators turned off.
To disable the indicators, mouse over "Options" (top-left of screen), then "Styles", To disable the indicators, mouse over "Options" (top-left of screen), then "Styles",
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 probably best off doing the following in WSL2) (_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. )
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/),
@ -140,7 +197,7 @@ Here's how:
2. Install [trunk](https://trunkrs.dev/) 2. Install [trunk](https://trunkrs.dev/)
- Run `cargo install --locked trunk` - Run `cargo install --locked trunk`
3. Clone this repository using git 3. Clone this repository using git
4. Enter the project directory and run `trunk serve` 4. Enter the plate-tool-web directory and run `trunk serve`
- You may need to check where `cargo` is installing binaries by default. For me, they're at `~/.cargo/bin`. - You may need to check where `cargo` is installing binaries by default. For me, they're at `~/.cargo/bin`.
If trunk is not automatically placed in your path, you would then run `/your/path/to/.cargo/bin/trunk serve`. If trunk is not automatically placed in your path, you would then run `/your/path/to/.cargo/bin/trunk serve`.
- You can instead run `trunk build --release` for a more performant binary. - You can instead run `trunk build --release` for a more performant binary.

View File

@ -1,18 +0,0 @@
@use "sass:color";
@use "../variables" as *;
div.plate_container {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
border: 2px solid $color-dark;
grid-column: right / right;
grid-row: upper / 3;
h2 {
margin-bottom: 1%;
text-align: center;
}
}

2
plate-tool-lib/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/dist

339
plate-tool-lib/Cargo.lock generated Normal file
View File

@ -0,0 +1,339 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "js-sys"
version = "0.3.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "memchr"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "plate-tool-lib"
version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"log",
"rand",
"regex",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "regex"
version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "ryu"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "serde"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "uuid"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
dependencies = [
"atomic",
"getrandom",
"rand",
"serde",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"

19
plate-tool-lib/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "plate-tool-lib"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
regex = "1"
uuid = { version = "1.6", features = ["v7", "fast-rng", "macro-diagnostics", "js", "serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.2"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] }
lazy_static = "1.4"
serde_with = { version = "3.9.0", features = ["json"] }
serde_yaml = "0.9.34"

View File

@ -0,0 +1,27 @@
use serde::Serialize;
use super::TransferRecord;
/// Format preferred by the Echo Client when using the Pick-List function.
///
/// Note that this does not export plate info!
/// Exports of this type should combine all transfers between the two actively selected plates.
#[derive(Serialize, Debug)]
pub struct EchoClientTransferRecord {
#[serde(rename = "SrcWell")]
pub source_well: String,
#[serde(rename = "DestWell")]
pub destination_well: String,
#[serde(rename = "XferVol")]
pub volume: f32,
}
impl From<TransferRecord> for EchoClientTransferRecord {
fn from(value: TransferRecord) -> Self {
EchoClientTransferRecord {
source_well: value.source_well,
destination_well: value.destination_well,
volume: value.volume,
}
}
}

View File

@ -0,0 +1,231 @@
//! 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 crate::plate::{PlateFormat, PlateType};
use crate::plate_instances::PlateInstance;
use crate::transfer::Transfer;
use crate::transfer_volume::{TransferVolume, VolumeMaps};
use crate::Well;
use crate::transfer_region::{CustomRegion, Region, TransferRegion};
use std::collections::{HashSet, HashMap};
pub struct AutoOutput {
pub sources: Vec<PlateInstance>,
pub destinations: Vec<PlateInstance>,
pub transfers: Vec<Transfer>,
}
/// Two lists of plates that are guaranteed to be unique (no duplicate plates)
struct UniquePlates {
sources: Vec<PlateInstance>,
destinations: Vec<PlateInstance>,
}
const W96_COL_MAX: u8 = 12;
const W96_ROW_MAX: u8 = 8;
const W384_COL_MAX: u8 = 24;
const W384_ROW_MAX: u8 = 16;
const W1536_COL_MAX: u8 = 48;
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 {
let unique_plates = find_unique_plates(records);
let transfers = get_transfer_for_all_pairs(records, &unique_plates);
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 {
let transfer_records = read_csv(data);
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 {
let mut source_names: HashSet<&str> = HashSet::new();
let mut destination_names: HashSet<&str> = HashSet::new();
for record in records {
source_names.insert(&record.source_plate);
destination_names.insert(&record.destination_plate);
}
let mut sources: Vec<PlateInstance> = Vec::with_capacity(source_names.len());
for source_name in source_names {
let filtered_records: Vec<&TransferRecord> = records
.iter()
.filter(|x| x.source_plate == source_name)
.collect();
let format_guess = guess_plate_size(&filtered_records, PlateType::Source);
sources.push(PlateInstance::new(
PlateType::Source,
format_guess,
source_name.to_string(),
))
}
let mut destinations: Vec<PlateInstance> = Vec::with_capacity(destination_names.len());
for destination_name in destination_names {
let filtered_records: Vec<&TransferRecord> = records
.iter()
.filter(|x| x.destination_plate == destination_name)
.collect();
let format_guess = guess_plate_size(&filtered_records, PlateType::Destination);
destinations.push(PlateInstance::new(
PlateType::Destination,
format_guess,
destination_name.to_string(),
))
}
UniquePlates {
sources,
destinations,
}
}
/// 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 {
let mut guess = PlateFormat::W96; // Never guess smaller than 96
for record in plate_filtered_records {
if let Some(Well { row, col }) = string_well_to_pt(match which {
PlateType::Source => &record.source_well,
PlateType::Destination => &record.destination_well,
}) {
if row > W1536_ROW_MAX || col > W1536_COL_MAX {
return PlateFormat::W3456;
} else if row > W384_ROW_MAX || col > W384_COL_MAX {
guess = PlateFormat::W1536;
} else if row > W96_ROW_MAX || col > W96_COL_MAX {
guess = PlateFormat::W384;
}
}
}
guess
}
fn get_transfer_for_all_pairs(
records: &[TransferRecord],
unique_plates: &UniquePlates,
) -> Vec<Transfer> {
let mut transfers: Vec<Transfer> = Vec::new();
for source_instance in &unique_plates.sources {
for destination_instance in &unique_plates.destinations {
if let Some(transfer) =
get_transfer_for_pair(records, source_instance, destination_instance)
{
transfers.push(transfer);
}
}
}
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(
records: &[TransferRecord],
source: &PlateInstance,
destination: &PlateInstance,
) -> Option<Transfer> {
let source_name: &str = &source.name;
let destination_name: &str = &destination.name;
let mut filtered_records = records
.iter()
.filter(|x| x.source_plate == source_name && x.destination_plate == destination_name)
.peekable();
filtered_records.peek()?;
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();
for record in filtered_records {
let source_point_opt = string_well_to_pt(&record.source_well);
let destination_point_opt = string_well_to_pt(&record.destination_well);
if source_point_opt.and(destination_point_opt).is_some() {
let source_point = source_point_opt.unwrap();
let destination_point = destination_point_opt.unwrap();
source_wells.insert(source_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 destination_wells_vec: Vec<Well> = 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 =
Region::Custom(CustomRegion::new(source_wells_vec, destination_wells_vec));
let transfer_region = TransferRegion {
source_plate: source.plate,
dest_plate: destination.plate,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_region: custom_region.clone(),
dest_region: custom_region,
};
let transfer_name = format!("{} to {}", source.name, destination.name);
let mut transfer = Transfer::new(
source.clone(),
destination.clone(),
transfer_region,
transfer_name,
);
transfer.volume = transfer_volume;
Some(transfer)
}

View File

@ -0,0 +1,109 @@
use crate::transfer_volume::TransferVolume;
use crate::util::*;
use crate::{transfer::Transfer, Well};
use super::{
alternative_formats::EchoClientTransferRecord, mangle_headers::mangle_headers,
transfer_record::TransferRecordDeserializeIntermediate, TransferRecord,
};
use lazy_static::lazy_static;
use regex::Regex;
use std::error::Error;
pub fn transfer_to_records(
tr: &Transfer,
src_barcode: &str,
dest_barcode: &str,
) -> Vec<TransferRecord> {
let source_wells = tr.transfer_region.get_source_wells();
let map = tr.transfer_region.calculate_map();
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 {
let dest_wells = map(s_well);
if let Some(dest_wells) = 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 {
source_plate: src_barcode.to_string(),
source_well: format!("{}{}", num_to_letters(s_well.row).unwrap(), s_well.col),
destination_plate: dest_barcode.to_string(),
destination_well: format!(
"{}{}",
num_to_letters(d_well.row).unwrap(),
d_well.col
),
volume,
concentration: None,
})
}
}
}
records
}
pub fn records_to_csv(trs: Vec<TransferRecord>) -> Result<String, Box<dyn Error>> {
let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
for record in trs {
wtr.serialize(record)?
}
let data = String::from_utf8(wtr.into_inner()?)?;
Ok(data)
}
pub fn records_to_echo_client_csv(trs: Vec<TransferRecord>) -> Result<String, Box<dyn Error>> {
let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
for record in trs {
wtr.serialize(Into::<EchoClientTransferRecord>::into(record))?
}
let data = String::from_utf8(wtr.into_inner()?)?;
Ok(data)
}
/// Converts "spreadsheet format" well identification to coordinates
pub fn string_well_to_pt(input: &str) -> Option<Well> {
lazy_static! {
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();
}
if let Some(c1) = REGEX.captures(input) {
if let (Some(row), Some(col)) = (letters_to_num(&c1[1]), c1[2].parse::<u8>().ok()) {
return Some(Well { row, col });
} else {
return None;
}
}
None
}
pub fn read_csv(data: &str) -> Vec<TransferRecord> {
let modified = mangle_headers(data);
let mut rdr = csv::Reader::from_reader(modified.as_bytes());
let mut records: Vec<TransferRecord> = Vec::new();
for record in rdr.deserialize::<TransferRecordDeserializeIntermediate>() {
match record {
Ok(r) => {
if !r.is_empty() {
records.push(r.into());
}
}
Err(e) => {
log::debug!("{:?}", e);
}
}
}
records
}

View File

@ -0,0 +1,74 @@
//! 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 {
let (header, rows) = data.split_at(data.find('\n').unwrap());
let fields = header.trim().split(',');
let mut modified_headers: Vec<String> = Vec::new();
for field in fields {
if let Some(f) = detect_field(field) {
modified_headers.push(f.to_string());
} else {
log::debug!("Unk field: {:?}", field)
}
}
modified_headers.join(",") + "\n" + rows
}
fn detect_field(field: &str) -> Option<Field> {
// NOTE: Don't return none! Consider refactor to -> Field later on.
// Returning none means a field will be dropped, which requires the user to manually drop
// columns from their picklist.
match field.trim().to_lowercase() {
x if x.contains("source") || x.contains("src") => match x {
_ if x.contains("plate") || x.contains("barcode") => Some(Field::SourcePlate),
_ if x.contains("well") => Some(Field::SourceWell),
_ if x.contains("format") || x.contains("fmt") => Some(Field::SourceFormat),
_ => Some(Field::Other(x)), // Retain unknown fields
},
x if x.contains("destination") || x.contains("dest") => match x {
_ if x.contains("plate") || x.contains("barcode") => Some(Field::DestinationPlate),
_ if x.contains("well") => Some(Field::DestinationWell),
_ if x.contains("format") || x.contains("fmt") => Some(Field::DestinationFormat),
_ => Some(Field::Other(x)), // Retain unknown fields
},
x if x.contains("volume") => Some(Field::Volume),
x if x.contains("concentration") => Some(Field::Concentration),
x => Some(Field::Other(x)), // Retain unknown fields
}
}
enum Field {
SourcePlate,
DestinationPlate,
SourceWell,
DestinationWell,
SourceFormat,
DestinationFormat,
Volume,
Concentration,
Other(String),
}
impl ToString for Field {
fn to_string(&self) -> String {
match self {
Field::SourcePlate => "sourceplate".to_string(),
Field::DestinationPlate => "destinationplate".to_string(),
Field::SourceWell => "sourcewell".to_string(),
Field::DestinationWell => "destinationwell".to_string(),
Field::SourceFormat => "sourceformat".to_string(),
Field::DestinationFormat => "destinationformat".to_string(),
Field::Volume => "volume".to_string(),
Field::Concentration => "concentration".to_string(),
Field::Other(x) => x.to_string(),
}
}
}

View File

@ -0,0 +1,10 @@
mod transfer_record;
mod conversion;
mod auto;
mod mangle_headers;
mod alternative_formats;
pub use transfer_record::TransferRecord;
pub use conversion::*;
pub use auto::{auto, read_csv_auto};

View File

@ -0,0 +1,107 @@
use serde::{Deserialize, Serialize};
use crate::{plate::PlateFormat, 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)]
pub struct TransferRecord {
#[serde(rename = "Source Barcode")]
pub source_plate: String,
#[serde(rename = "Source Well")]
pub source_well: String,
#[serde(rename = "Dest Barcode")]
pub destination_plate: String,
#[serde(rename = "Destination Well")]
pub destination_well: String,
#[serde(rename = "Transfer Volume")]
pub volume: f32,
#[serde(rename = "Concentration")]
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)]
pub struct TransferRecordDeserializeIntermediate {
#[serde(rename = "sourceplate")]
source_plate: String,
#[serde(rename = "destinationplate")]
destination_plate: String,
#[serde(rename = "sourcewell")]
source_well: String,
#[serde(rename = "sourceformat")]
source_format: Option<String>,
#[serde(rename = "destinationwell")]
destination_well: String,
#[serde(rename = "destinationformat")]
destination_format: Option<String>,
#[serde(rename = "volume")]
volume: Option<f32>,
#[serde(rename = "concentration")]
concentration: Option<f32>,
}
impl From<TransferRecordDeserializeIntermediate> for TransferRecord {
fn from(value: TransferRecordDeserializeIntermediate) -> Self {
let mut source_well: String = value.source_well;
if let Some(pformat) = value
.source_format
.and_then(|x| PlateFormat::try_from(x.as_str()).ok())
{
if let Ok(well_number) = source_well.parse::<u16>() {
if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) {
source_well = alphanumeric;
}
}
}
let mut destination_well: String = value.destination_well;
if let Some(pformat) = value
.destination_format
.and_then(|x| PlateFormat::try_from(x.as_str()).ok())
{
if let Ok(well_number) = destination_well.parse::<u16>() {
if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) {
destination_well = alphanumeric;
}
}
}
let volume = value.volume.unwrap_or(2.5f32);
TransferRecord {
source_plate: value.source_plate,
destination_plate: value.destination_plate,
source_well,
destination_well,
volume,
concentration: value.concentration,
}
}
}
impl TransferRecordDeserializeIntermediate {
/// Used to cull malformed picklist entries that lack required information
pub fn is_empty(&self) -> bool {
self.source_plate.is_empty()
|| self.destination_plate.is_empty()
|| self.source_well.is_empty()
|| self.destination_well.is_empty()
}
}
fn numeric_well_to_alphanumeric(input: u16, pformat: PlateFormat) -> Option<String> {
let column_height: u16 = pformat.size().0 as u16;
let column = input.div_ceil(column_height);
let row = input % column_height;
let row_str = num_to_letters(row as u8)?;
Some(format!("{}{}", row_str, column))
}

View File

@ -3,3 +3,8 @@ 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;
mod well;
pub use well::Well;

View File

@ -60,6 +60,37 @@ impl std::fmt::Display for PlateFormat {
} }
} }
} }
impl TryFrom<&str> for PlateFormat {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
let lower = value.to_lowercase();
match lower.trim() {
"w6" | "6" => Ok(PlateFormat::W6),
"w12" | "12" => Ok(PlateFormat::W12),
"w24" | "24" => Ok(PlateFormat::W24),
"w48" | "48" => Ok(PlateFormat::W48),
"w96" | "96" => Ok(PlateFormat::W96),
"w384" | "384" => Ok(PlateFormat::W384),
"w1536" | "1536" => Ok(PlateFormat::W1536),
"w3456" | "3456" => Ok(PlateFormat::W3456),
_ => Err(())
}
}
}
impl From<&PlateFormat> for u16 {
fn from(value: &PlateFormat) -> Self {
match value {
PlateFormat::W6 => 6,
PlateFormat::W12 => 12,
PlateFormat::W24 => 24,
PlateFormat::W48 => 48,
PlateFormat::W96 => 96,
PlateFormat::W384 => 384,
PlateFormat::W1536 => 1536,
PlateFormat::W3456 => 3456,
}
}
}
impl PlateFormat { impl PlateFormat {
pub fn size(&self) -> (u8, u8) { pub fn size(&self) -> (u8, u8) {

View File

@ -2,7 +2,7 @@ use super::plate::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
#[derive(PartialEq, Clone, Serialize, Deserialize)] #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)]
pub struct PlateInstance { pub struct PlateInstance {
pub plate: Plate, pub plate: Plate,
#[serde(rename = "id_v7")] #[serde(rename = "id_v7")]
@ -30,6 +30,10 @@ impl PlateInstance {
pub fn change_name(&mut self, new_name: String) { pub fn change_name(&mut self, new_name: String) {
self.name = new_name; self.name = new_name;
} }
pub fn change_format(&mut self, new_format: &PlateFormat) {
self.plate.plate_format = *new_format;
}
} }
impl From<Plate> for PlateInstance { impl From<Plate> for PlateInstance {
@ -41,3 +45,10 @@ impl From<Plate> for PlateInstance {
} }
} }
} }
impl From<&PlateInstance> for String {
fn from(value: &PlateInstance) -> Self {
// Could have other formatting here
format!("{}, {}", value.name, value.plate.plate_format)
}
}

View File

@ -1,5 +1,8 @@
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;
@ -14,8 +17,7 @@ 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,
#[serde(default = "default_volume")] pub volume: TransferVolume,
pub volume: f32,
} }
impl Default for Transfer { impl Default for Transfer {
@ -26,15 +28,11 @@ 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: 2.5f32, volume: Default::default(),
} }
} }
} }
fn default_volume() -> f32 {
2.5f32
}
impl Transfer { impl Transfer {
pub fn new( pub fn new(
source: PlateInstance, source: PlateInstance,
@ -48,7 +46,7 @@ impl Transfer {
name, name,
id: Uuid::now_v7(), id: Uuid::now_v7(),
transfer_region: tr, transfer_region: tr,
volume: 2.5, volume: transfer_volume::TransferVolume::Single(2.5),
} }
} }

View File

@ -1,27 +1,32 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::components::transfer_menu::RegionDisplay;
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<(u8, u8)>, src: Vec<Well>,
dest: Vec<(u8, u8)>, dest: Vec<Well>,
}
impl CustomRegion {
pub fn new(src: Vec<Well>, dest: Vec<Well>) -> Self {
CustomRegion { src, dest }
}
} }
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum Region { pub enum Region {
Rect((u8, u8), (u8, u8)), Rect(Well, Well),
Point((u8, u8)), Point(Well),
Custom(CustomRegion), Custom(CustomRegion),
} }
impl Default for Region { impl Default for Region {
fn default() -> Self { fn default() -> Self {
Region::Point((1, 1)) Region::Point(Well { row: 1, col: 1 })
} }
} }
impl TryFrom<Region> for ((u8, u8), (u8, u8)) { impl TryFrom<Region> for (Well, Well) {
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 {
@ -32,10 +37,11 @@ impl TryFrom<Region> for ((u8, u8), (u8, u8)) {
} }
} }
} }
impl Region { impl Region {
pub fn new_custom(transfers: &Vec<((u8, u8), (u8, u8))>) -> Self { pub fn new_custom(transfers: &Vec<(Well, Well)>) -> Self {
let mut src_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len()); let mut src_pts: Vec<Well> = Vec::with_capacity(transfers.len());
let mut dest_pts: Vec<(u8, u8)> = Vec::with_capacity(transfers.len()); let mut dest_pts: Vec<Well> = Vec::with_capacity(transfers.len());
for transfer in transfers { for transfer in transfers {
src_pts.push(transfer.0); src_pts.push(transfer.0);
@ -73,11 +79,11 @@ impl Default for TransferRegion {
} }
impl TransferRegion { impl TransferRegion {
pub fn get_source_wells(&self) -> Vec<(u8, u8)> { pub fn get_source_wells(&self) -> Vec<Well> {
match &self.source_region { match &self.source_region {
Region::Rect(c1, c2) => { Region::Rect(c1, c2) => {
let mut wells = Vec::<(u8, u8)>::new(); let mut wells = Vec::<Well>::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!
// We'll reassign these values (still not mutable) just in case. // We'll reassign these values (still not mutable) just in case.
@ -86,12 +92,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.0..=br.0).step_by(i8::abs(interleave_i) as usize) { for i in (ul.row..=br.row).step_by(i8::abs(interleave_i) as usize) {
for j in (ul.1..=br.1).step_by(i8::abs(interleave_j) as usize) { for j in (ul.col..=br.col).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((i, j)) wells.push(Well { row: i, col: j })
} }
} }
wells wells
@ -101,33 +107,35 @@ impl TransferRegion {
} }
} }
pub fn get_destination_wells(&self) -> Vec<(u8, u8)> { pub fn get_destination_wells(&self) -> Vec<Well> {
let map = self.calculate_map(); match &self.source_region {
let source_wells = self.get_source_wells(); Region::Custom(c) => c.dest.clone(),
_ => {
let map = self.calculate_map();
let source_wells = self.get_source_wells();
let mut wells = Vec::<(u8, u8)>::new(); let mut wells = Vec::<Well>::new();
// log::debug!("GDW:"); 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) { wells.append(&mut dest_wells);
// log::debug!("Map {:?} to {:?}", well, dest_wells); }
wells.append(&mut dest_wells); }
wells
} }
} }
// log::debug!("GDW END.");
wells
} }
#[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((u8, u8)) -> Option<Vec<(u8, u8)>> + '_> { pub fn calculate_map(&self) -> Box<dyn Fn(Well) -> Option<Vec<Well>> + '_> {
// 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);
@ -135,10 +143,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: ((u8, u8), (u8, u8)) = match self.source_region { let source_corners: (Well, Well) = match self.source_region {
Region::Point((x, y)) => ((x, y), (x, y)), Region::Point(w) => (w, w),
Region::Rect(c1, c2) => (c1, c2), Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0, 0), (0, 0)), Region::Custom(_) => (Well { row: 0, col: 0 }, Well { row: 0, col: 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,
@ -148,45 +156,46 @@ impl TransferRegion {
// Non-replicate transfers: // Non-replicate transfers:
match &self.dest_region { match &self.dest_region {
Region::Point((x, y)) => { Region::Point(Well { row: x, col: y }) => {
Box::new(move |(i, j)| { Box::new(move |Well { row: i, col: j }| {
if source_wells.contains(&(i, j)) { if source_wells.contains(&Well { row: i, col: j }) {
// Validity here already checked by self.validate() // Validity here already checked by self.validate()
Some(vec![( Some(vec![Well {
x + i row: x + i
.checked_sub(source_ul.0) .checked_sub(source_ul.row)
.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()),
y + j col: y + j
.checked_sub(source_ul.1) .checked_sub(source_ul.col)
.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 |(i, j)| { Box::new(move |w| {
if source_wells.contains(&(i, j)) { let Well { row: i, col: j } = w;
let possible_destination_wells = create_dense_rectangle(&c1, &c2); if source_wells.contains(&w) {
let (d_ul, d_br) = standardize_rectangle(&c1, &c2); let possible_destination_wells = create_dense_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.0.checked_sub(s_ul.0).unwrap() + 1, s_br.row.checked_sub(s_ul.row).unwrap() + 1,
s_br.1.checked_sub(s_ul.1).unwrap() + 1, s_br.col.checked_sub(s_ul.col).unwrap() + 1,
); );
let d_dims = ( let d_dims = (
d_br.0.checked_sub(d_ul.0).unwrap() + 1, d_br.row.checked_sub(d_ul.row).unwrap() + 1,
d_br.1.checked_sub(d_ul.1).unwrap() + 1, d_br.col.checked_sub(d_ul.col).unwrap() + 1,
); );
let N_s = ( let number_used_src_wells = (
// Number of used source wells // Number of used source wells
(s_dims.0 + il_source.0.unsigned_abs() - 1) (s_dims.0 + il_source.0.unsigned_abs() - 1)
.div_euclid(il_source.0.unsigned_abs()), .div_euclid(il_source.0.unsigned_abs()),
@ -197,52 +206,52 @@ impl TransferRegion {
// How many times can we replicate? // How many times can we replicate?
(1..) (1..)
.position(|n| { .position(|n| {
n * N_s.0 * il_dest.0.unsigned_abs() - il_dest.0.unsigned_abs() n * number_used_src_wells.0 * il_dest.0.unsigned_abs()
- il_dest.0.unsigned_abs()
+ 1 + 1
> d_dims.0 > d_dims.0
}) })
.unwrap() as u8, .unwrap() as u8,
(1..) (1..)
.position(|n| { .position(|n| {
n * N_s.1 * il_dest.1.unsigned_abs() - il_dest.1.unsigned_abs() n * number_used_src_wells.1 * il_dest.1.unsigned_abs()
- il_dest.1.unsigned_abs()
+ 1 + 1
> d_dims.1 > d_dims.1
}) })
.unwrap() as u8, .unwrap() as u8,
); );
let i = i let i = i
.saturating_sub(s_ul.0) .saturating_sub(s_ul.row)
.saturating_div(il_source.0.unsigned_abs()); .saturating_div(il_source.0.unsigned_abs());
let j = j let j = j
.saturating_sub(s_ul.1) .saturating_sub(s_ul.col)
.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(|(x, _)| { .filter(|Well { row: x , ..}| {
x.checked_sub(d_ul.0).unwrap() x.checked_sub(d_ul.row).unwrap()
% (N_s.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)
% (N_s.0 * il_dest.0.unsigned_abs()) % (number_used_src_wells.0 * il_dest.0.unsigned_abs())
}) })
.filter(|(_, y)| { .filter(|Well { col: y, .. }| {
y.checked_sub(d_ul.1).unwrap() y.checked_sub(d_ul.col).unwrap()
% (N_s.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)
% (N_s.1 * il_dest.1.unsigned_abs()) % (number_used_src_wells.1 * il_dest.1.unsigned_abs())
}) })
.filter(|(x, y)| { .filter(|Well { row: x, col: 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.0) x.checked_sub(d_ul.row).unwrap().div_euclid(
.unwrap() number_used_src_wells.0 * il_dest.0.unsigned_abs(),
.div_euclid(N_s.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) number_used_src_wells.1 * il_dest.1.unsigned_abs(),
.unwrap() ) < count.1
.div_euclid(N_s.1 * il_dest.1.unsigned_abs())
< count.1
}) })
.collect(), .collect(),
) )
@ -251,14 +260,14 @@ impl TransferRegion {
} }
}) })
} }
Region::Custom(c) => Box::new(move |(i, j)| { Region::Custom(c) => Box::new(move |Well { row: i, col: j }| {
let src = c.src.clone(); let src = c.src.clone();
let dest = c.dest.clone(); let dest = c.dest.clone();
let points: Vec<(u8, u8)> = src let points: Vec<Well> = src
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_index, (x, y))| *x == i && *y == j) .filter(|(_index, Well { row: x, col: y })| *x == i && *y == j)
.map(|(index, _)| dest[index]) .map(|(index, _)| dest[index])
.collect(); .collect();
if points.is_empty() { if points.is_empty() {
@ -287,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.0 == 0 || s1.1 == 0 || s2.0 == 0 || s2.1 == 0 { if s1.row == 0 || s1.col == 0 || s2.row == 0 || s2.col == 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.0 > source_max.0 || s2.0 > source_max.0 { if s1.row > source_max.0 || s2.row > 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.1 > source_max.1 || s2.1 > source_max.1 { if s1.col > source_max.1 || s2.col > 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)");
} }
@ -316,28 +325,34 @@ impl TransferRegion {
} }
} }
fn create_dense_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> Vec<(u8, u8)> { fn create_dense_rectangle(c1: &Well, c2: &Well) -> Vec<Well> {
// 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::<(u8, u8)>::new(); let mut points = Vec::<Well>::new();
for i in c1.0..=c2.0 { for i in c1.row..=c2.row {
for j in c1.1..=c2.1 { for j in c1.col..=c2.col {
points.push((i, j)); points.push(Well { row: i, col: j });
} }
} }
points points
} }
fn standardize_rectangle(c1: &(u8, u8), c2: &(u8, u8)) -> ((u8, u8), (u8, u8)) { fn standardize_rectangle(c1: &Well, c2: &Well) -> (Well, Well) {
let upper_left_i = u8::min(c1.0, c2.0); let upper_left_i = u8::min(c1.row, c2.row);
let upper_left_j = u8::min(c1.1, c2.1); let upper_left_j = u8::min(c1.col, c2.col);
let bottom_right_i = u8::max(c1.0, c2.0); let bottom_right_i = u8::max(c1.row, c2.row);
let bottom_right_j = u8::max(c1.1, c2.1); let bottom_right_j = u8::max(c1.col, c2.col);
( (
(upper_left_i, upper_left_j), Well {
(bottom_right_i, bottom_right_j), row: upper_left_i,
col: upper_left_j,
},
Well {
row: bottom_right_i,
col: bottom_right_j,
},
) )
} }
@ -354,7 +369,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(&(i, j)) { if source_wells.contains(&Well { row: i, col: j }) {
source_string.push('x') source_string.push('x')
} else { } else {
source_string.push('.') source_string.push('.')
@ -370,7 +385,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(&(i, j)) { if dest_wells.contains(&Well { row: i, col: j }) {
dest_string.push('x') dest_string.push('x')
} else { } else {
dest_string.push('.') dest_string.push('.')
@ -384,134 +399,130 @@ impl fmt::Display for TransferRegion {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use wasm_bindgen_test::*; use crate::plate::*;
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((1, 1), (3, 3)), source_region: Region::Rect(Well{ row: 1, col: 1}, Well { row: 1, col: 1 }),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point((3, 3)), dest_region: Region::Point(Well {row: 3, col: 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((1, 1)), transfer1_map(Well {row: 1, col: 1}),
Some(vec! {(3,3)}), Some(vec! {Well {row: 3, col: 3}}),
"Failed basic shift transfer 1" "Failed basic shift transfer 1"
); );
assert_eq!( assert_eq!(
transfer1_map((1, 2)), transfer1_map(Well{ row: 1, col: 2}),
Some(vec! {(3,4)}), Some(vec! {Well{ row: 3, col: 4}}),
"Failed basic shift transfer 2" "Failed basic shift transfer 2"
); );
assert_eq!( assert_eq!(
transfer1_map((2, 2)), transfer1_map(Well{ row: 2, col: 2}),
Some(vec! {(4,4)}), Some(vec! {Well{ row: 4, col: 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((1, 1), (3, 3)), source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 3, col: 3}),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point((3, 3)), dest_region: Region::Point(Well{ row: 3, col: 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((1, 1)), transfer2_map(Well{ row: 1, col: 1}),
Some(vec! {(3,3)}), Some(vec! {Well{ row: 3, col: 3}}),
"Failed source interleave, type simple 1" "Failed source interleave, type simple 1"
); );
assert_eq!( assert_eq!(
transfer2_map((1, 2)), transfer2_map(Well{ row: 1, col: 2}),
None, None,
"Failed source interleave, type simple 2" "Failed source interleave, type simple 2"
); );
assert_eq!( assert_eq!(
transfer2_map((2, 2)), transfer2_map(Well{ row: 2, col: 2}),
None, None,
"Failed source interleave, type simple 3" "Failed source interleave, type simple 3"
); );
assert_eq!( assert_eq!(
transfer2_map((3, 3)), transfer2_map(Well{ row: 3, col: 3}),
Some(vec! {(4,4)}), Some(vec! {Well{ row: 4, col: 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((1, 1), (3, 3)), source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 3, col: 3}),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Point((3, 3)), dest_region: Region::Point(Well{ row: 3, col: 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((1, 1)), transfer3_map(Well{ row: 1, col: 1}),
Some(vec! {(3,3)}), Some(vec! {Well{ row: 3, col: 3}}),
"Failed destination interleave, type simple 1" "Failed destination interleave, type simple 1"
); );
assert_eq!( assert_eq!(
transfer3_map((2, 1)), transfer3_map(Well{ row: 2, col: 1}),
Some(vec! {(5,3)}), Some(vec! {Well{ row: 5, col: 3}}),
"Failed destination interleave, type simple 2" "Failed destination interleave, type simple 2"
); );
assert_eq!( assert_eq!(
transfer3_map((1, 2)), transfer3_map(Well{ row: 1, col: 2}),
Some(vec! {(3,6)}), Some(vec! {Well{ row: 3, col: 6}}),
"Failed destination interleave, type simple 3" "Failed destination interleave, type simple 3"
); );
assert_eq!( assert_eq!(
transfer3_map((2, 2)), transfer3_map(Well{ row: 2, col: 2}),
Some(vec! {(5,6)}), Some(vec! {Well{ row: 5, col: 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((1, 1), (2, 2)), source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 2, col: 2}),
dest_plate: destination, dest_plate: destination,
dest_region: Region::Rect((2, 2), (11, 11)), dest_region: Region::Rect(Well{ row: 2, col: 2}, Well{row:11, col: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((1, 1)), transfer1_map(Well{ row: 1, col: 1}),
Some(vec! {(2, 2), (2, 8), (8, 2), (8, 8)}), Some(vec! {Well{ row: 2, col: 2}, Well{ row: 2, col: 8}, Well{ row: 8, col: 2}, Well{ row: 8, col: 8}}),
"Failed type replicate 1" "Failed type replicate 1"
); );
assert_eq!( assert_eq!(
transfer1_map((2, 1)), transfer1_map(Well{ row: 2, col: 1}),
Some(vec! {(5, 2), (5, 8), (11, 2), (11, 8)}), Some(vec! {Well{ row: 5, col: 2}, Well{ row: 5, col: 8}, Well{ row: 11, col: 2}, Well{ row: 11, col: 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((1, 1), (2, 3)), source_region: Region::Rect(Well{ row: 1, col: 1}, Well{ row: 2, col: 3}),
dest_region: Region::Rect((2, 2), (11, 16)), dest_region: Region::Rect(Well{ row: 2, col: 2}, Well{ row: 11, col: 16}),
interleave_source: (1, 1), interleave_source: (1, 1),
interleave_dest: (2, 2), interleave_dest: (2, 2),
}; };
@ -519,71 +530,69 @@ 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![(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)], 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}],
"Failed type replicate 2 source" "Failed type replicate 2 source"
); );
assert_eq!( assert_eq!(
transfer2_dest, transfer2_dest,
vec![ vec![
(2, 2), Well{ row: 2, col: 2},
(2, 8), Well{ row: 2, col: 8},
(6, 2), Well{ row: 6, col: 2},
(6, 8), Well{ row: 6, col: 8},
(2, 4), Well{ row: 2, col: 4},
(2, 10), Well{ row: 2, col: 10},
(6, 4), Well{ row: 6, col: 4},
(6, 10), Well{ row: 6, col: 10},
(2, 6), Well{ row: 2, col: 6},
(2, 12), Well{ row: 2, col: 12},
(6, 6), Well{ row: 6, col: 6},
(6, 12), Well{ row: 6, col: 12},
(4, 2), Well{ row: 4, col: 2},
(4, 8), Well{ row: 4, col: 8},
(8, 2), Well{ row: 8, col: 2},
(8, 8), Well{ row: 8, col: 8},
(4, 4), Well{ row: 4, col: 4},
(4, 10), Well{ row: 4, col: 10},
(8, 4), Well{ row: 8, col: 4},
(8, 10), Well{ row: 8, col: 10},
(4, 6), Well{ row: 4, col: 6},
(4, 12), Well{ row: 4, col: 12},
(8, 6), Well{ row: 8, col: 6},
(8, 12) Well{ row: 8, col: 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((1, 4), (3, 7)), source_region: Region::Rect(Well{ row: 1, col: 4}, Well{ row: 3, col: 7}),
dest_region: Region::Point((1, 9)), dest_region: Region::Point(Well{ row: 1, col: 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 mut transfer1_dest = transfer1.get_destination_wells(); let transfer1_dest: HashSet<Well> = transfer1.get_destination_wells().into_iter().collect();
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![(1, 9), (1, 11), (1, 13), (1, 15)], vec![Well{ row: 1, col: 9}, Well{ row: 1, col: 11}, Well{ row: 1, col: 13}, Well{ row: 1, col: 15}].into_iter().collect(),
"Failed type pool 1 dest" "Failed type pool 1 dest"
); );
assert_eq!( assert_eq!(
transfer1_map((2, 6)), transfer1_map(Well{ row: 2, col: 6}),
Some(vec![(1, 13)]), Some(vec![Well{ row: 1, col: 13}]),
"Failed type pool 1 map 1" "Failed type pool 1 map 1"
); );
assert_eq!( assert_eq!(
transfer1_map((3, 7)), transfer1_map(Well{ row: 3, col: 7}),
Some(vec![(1, 15)]), Some(vec![Well{ row: 1, col: 15}]),
"Failed type pool 1 map 2" "Failed type pool 1 map 2"
); );
} }

View File

@ -0,0 +1,30 @@
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

@ -0,0 +1,69 @@
pub fn letters_to_num(letters: &str) -> Option<u8> {
let mut num: u8 = 0;
for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() {
let n = letter as u8;
if !(65..=90).contains(&n) {
return None;
}
num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?;
}
Some(num)
}
pub fn num_to_letters(num: u8) -> Option<String> {
if num == 0 {
return None;
} // Otherwise, we will not return none!
// As another note, we can't represent higher than "IV" anyway;
// thus there's no reason for a loop (26^n with n>1 will NOT occur).
let mut text = "".to_string();
let mut digit1 = num.div_euclid(26u8);
let mut digit2 = num.rem_euclid(26u8);
if digit1 > 0 && digit2 == 0u8 {
digit1 -= 1;
digit2 = 26;
}
if digit1 != 0 {
text.push((64 + digit1) as char)
}
text.push((64 + digit2) as char);
Some(text.to_string())
}
#[cfg(test)]
mod tests {
use super::{letters_to_num, num_to_letters};
#[test]
fn test_letters_to_num() {
assert_eq!(letters_to_num("D"), Some(4));
assert_eq!(letters_to_num("d"), None);
assert_eq!(letters_to_num("AD"), Some(26 + 4));
assert_eq!(letters_to_num("CG"), Some(3 * 26 + 7));
}
#[test]
fn test_num_to_letters() {
println!("27 is {:?}", num_to_letters(27));
assert_eq!(num_to_letters(1), Some("A".to_string()));
assert_eq!(num_to_letters(26), Some("Z".to_string()));
assert_eq!(num_to_letters(27), Some("AA".to_string()));
assert_eq!(num_to_letters(111), Some("DG".to_string()));
}
#[test]
fn test_l2n_and_n2l() {
assert_eq!(
num_to_letters(letters_to_num("A").unwrap()),
Some("A".to_string())
);
assert_eq!(
num_to_letters(letters_to_num("BJ").unwrap()),
Some("BJ".to_string())
);
for i in 1..=255 {
assert_eq!(letters_to_num(&num_to_letters(i).unwrap()), Some(i));
}
}
}

View File

@ -0,0 +1,9 @@
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,
}

2
plate-tool-web/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/dist

30
plate-tool-web/Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "plate-tool-web"
version = "0.5.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
plate-tool-lib = { path = "../plate-tool-lib" }
yew = { version = "0.20.0", features = ["csr"] }
yewdux = "0.9"
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["FormData", "HtmlFormElement",
"HtmlDialogElement", "Blob", "Url", "Window",
"HtmlAnchorElement", "ReadableStream", "HtmlSelectElement", "HtmlOptionElement", "HtmlButtonElement",
"FileReader", "HtmlCollection"] }
js-sys = "0.3"
log = "0.4"
wasm-logger = "0.2"
regex = "1"
lazy_static = "1.4"
uuid = { version = "1.6", features = ["v7", "fast-rng", "macro-diagnostics", "js", "serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
csv = "1.2"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.8", features = ["small_rng"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

View File

@ -0,0 +1,27 @@
@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

@ -7,6 +7,8 @@ dialog {
color: $color-dark; color: $color-dark;
background: $color-white; background: $color-white;
padding-right: 5%;
} }
dialog > form[method="dialog"] { dialog > form[method="dialog"] {
@ -35,3 +37,22 @@ dialog > form[method="dialog"] {
} }
} }
} }
.shifted_dialog {
top: 50vh;
left: 50vw;
margin: 0;
}
.close_button {
color: red;
position: absolute;
top: 5%;
right: 2%;
background: rgba(0%,0%,0%,10%);
&:hover {
color: rgb(0%,100%,100%);
background: rgba(0%,0%,0%,80%);
}
}

View File

@ -0,0 +1,61 @@
@use "sass:color";
@use "../variables" as *;
div.plate_container {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
border: 2px solid $color-dark;
grid-column: right / right;
grid-row: upper / 3;
h2 {
margin-bottom: 1%;
text-align: center;
}
&>div {
display: grid;
grid-template-rows: auto auto;
grid-template-rows: auto auto;
&>h2:nth-of-type(1) {
grid-column: 2;
grid-row: 1;
}
&>h2:nth-of-type(2) {
grid-column: 1;
grid-row: 2;
writing-mode: vertical-rl;
transform: rotate(-180deg);
}
&>div {
grid-column: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,5 +1,6 @@
@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;
@ -40,6 +41,11 @@ 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;
@ -50,10 +56,10 @@ input {
padding: 0; padding: 0;
&[type="text"] { &[type="text"] {
width: 4em; width: 6em;
} }
&[name="name"] { &[name="name"] {
width: 6em; // Override above width: calc(100% - 6em); // Override above
} }
&[type="number"] { &[type="number"] {
width: 2em; width: 2em;

View File

@ -1,5 +1,6 @@
@use "sass:color"; @use "sass:color";
@use "../variables" as *; @use "../variables" as *;
@use "../button" as *;
$selection-border-width: 2px; $selection-border-width: 2px;
@ -53,3 +54,17 @@ 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

@ -30,6 +30,7 @@ div.upper_menu {
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
font-size: calc($menu-height*0.7); font-size: calc($menu-height*0.7);
width: 100%;
} }
* { * {

View File

@ -0,0 +1,63 @@
#![allow(non_snake_case)]
use js_sys::Array;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, Url,
};
use yew::prelude::*;
use crate::components::states::MainState;
use crate::state_to_csv;
// type NoParamsCallback = Box<dyn Fn(())>;
pub fn export_csv_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if main_state.transfers.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("No transfers to export.")
.unwrap();
return;
}
if let Ok(csv) = state_to_csv(&main_state) {
save_str(&csv, "transfers.csv");
}
})
}
pub fn export_json_button_callback(main_state: std::rc::Rc<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
if let Ok(json) = serde_json::to_string(&main_state) {
save_str(&json, "plate-tool-state.json");
} else {
web_sys::window()
.unwrap()
.alert_with_message("Failed to export.")
.unwrap();
}
})
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}

View File

@ -0,0 +1,306 @@
use std::collections::HashSet;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
FileReader, HtmlButtonElement, HtmlDialogElement, HtmlElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use plate_tool_lib::transfer::Transfer;
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 super::create_close_button;
pub fn import_transfer_csv_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = import_transfer_csv_onload_callback(main_dispatch, fr1, modal);
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
}
pub fn import_transfer_csv_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".csv");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
import_transfer_csv_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}
pub fn import_transfer_csv_onload_callback(
main_dispatch: Dispatch<MainState>,
file_reader: FileReader,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &file_reader.result().ok().and_then(|v| v.as_string()) {
let records = plate_tool_lib::csv::read_csv(value);
let mut sources: HashSet<String> = HashSet::new();
let mut destinations: HashSet<String> = HashSet::new();
for record in records.iter() {
sources.insert(record.source_plate.clone());
destinations.insert(record.destination_plate.clone());
}
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let auto_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
auto_button.set_inner_text("Auto");
let auto_button_callback = auto_callback(main_dispatch.clone(), &records);
auto_button.set_onclick(Some(auto_button_callback.as_ref().unchecked_ref()));
auto_button_callback.forget();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let from_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in sources {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&source);
option.set_text(&source);
from_source.append_child(&option).unwrap();
}
let to_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in &main_dispatch.get().source_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&source.name);
option.set_text(&source.name);
to_source.append_child(&option).unwrap();
}
let from_dest = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for dest in destinations {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&dest);
option.set_text(&dest);
from_dest.append_child(&option).unwrap();
}
let to_dest = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for dest in &main_dispatch.get().destination_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&dest.name);
option.set_text(&dest.name);
to_dest.append_child(&option).unwrap();
}
let submit = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlButtonElement>()
.unwrap();
submit.set_value("Submit");
submit.set_inner_text("Submit");
let submit_callback = {
let main_dispatch = main_dispatch.clone();
let from_source = from_source.clone();
let to_source = to_source.clone();
let from_dest = from_dest.clone();
let to_dest = to_dest.clone();
import_transfer_csv_submit_callback(
main_dispatch,
from_source,
to_source,
from_dest,
to_dest,
records,
)
};
submit.set_onclick(Some(submit_callback.as_ref().unchecked_ref()));
submit_callback.forget();
form.append_child(&from_source).unwrap();
form.append_child(&to_source).unwrap();
form.append_child(&from_dest).unwrap();
form.append_child(&to_dest).unwrap();
modal.append_child(&submit).unwrap();
modal.append_child(&form).unwrap();
modal.append_child(&auto_button).unwrap();
}
})
}
pub fn import_transfer_csv_submit_callback(
main_dispatch: Dispatch<MainState>,
from_source: HtmlSelectElement,
to_source: HtmlSelectElement,
from_dest: HtmlSelectElement,
to_dest: HtmlSelectElement,
records: Vec<TransferRecord>,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let from_source = from_source.value();
let to_source = to_source.value();
let from_dest = from_dest.value();
let to_dest = to_dest.value();
let records: Vec<(Well, Well)> = records
.iter()
.filter(|record| record.source_plate == from_source)
.filter(|record| record.destination_plate == from_dest)
.map(|record| {
(
string_well_to_pt(&record.source_well).unwrap(),
string_well_to_pt(&record.destination_well).unwrap(),
)
})
.collect();
let spi = main_dispatch
.get()
.source_plates
.iter()
.find(|src| src.name == to_source)
.unwrap()
.clone();
let dpi = main_dispatch
.get()
.destination_plates
.iter()
.find(|dest| dest.name == to_dest)
.unwrap()
.clone();
let custom_region = Region::new_custom(&records);
let transfer_region = TransferRegion {
source_region: custom_region.clone(),
dest_region: custom_region,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_plate: spi.plate,
dest_plate: dpi.plate,
};
let transfer = Transfer::new(spi, dpi, transfer_region, "Custom Transfer".to_string());
main_dispatch.reduce_mut(|state| {
state.transfers.push(transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
})
}
fn auto_callback(
main_dispatch: Dispatch<MainState>,
records: &[TransferRecord],
) -> Closure<dyn FnMut(Event)> {
let records = Vec::from(records);
Closure::<dyn FnMut(_)>::new(move |_| {
let res = auto(&records);
main_dispatch.reduce_mut(|state| {
state.source_plates.extend(res.sources);
state
.destination_plates
.extend(res.destinations);
state.transfers.extend(res.transfers);
});
})
}

View File

@ -0,0 +1,96 @@
#![allow(non_snake_case)]
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{HtmlDialogElement, HtmlFormElement, HtmlInputElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use super::create_close_button;
pub fn input_json_input_callback(
main_dispatch: Dispatch<MainState>,
modal: HtmlDialogElement,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) = &fr1.result().ok().and_then(|v| v.as_string()) {
let ms = serde_json::from_str::<MainState>(value);
match ms {
Ok(ms) => main_dispatch.set(ms),
Err(e) => log::debug!("{:?}", e),
};
modal.close();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
}
pub fn import_json_button_callback(main_dispatch: Dispatch<MainState>) -> Callback<MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".json");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
input_json_input_callback(main_dispatch, modal)
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}

View File

@ -0,0 +1,18 @@
mod export_callbacks;
mod input_json_callbacks;
mod import_csv_callbacks;
mod settings_callbacks;
mod plate_edits_callbacks;
mod util;
pub use util::*;
pub use export_callbacks::*;
pub use input_json_callbacks::*;
pub use import_csv_callbacks::*;
pub use settings_callbacks::*;
pub use plate_edits_callbacks::*;

View File

@ -0,0 +1,43 @@
#![allow(non_snake_case)]
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::{new_plate_dialog::{NewPlateDialogType}, states::{CurrentTransfer, MainState}};
type NoParamsCallback = Box<dyn Fn(())>;
pub fn new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>,
) -> NoParamsCallback {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |_| {
new_plate_dialog_is_open.set(None);
})
}
pub fn open_new_plate_dialog_callback(
new_plate_dialog_is_open: UseStateHandle<Option<NewPlateDialogType>>
) -> Box<dyn Fn(NewPlateDialogType)> {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Box::new(move |dt| {
new_plate_dialog_is_open.set(Some(dt));
})
}
pub fn new_button_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<web_sys::MouseEvent> {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let confirm =
window.confirm_with_message("This will reset all plates and transfers. Proceed?");
if let Ok(confirm) = confirm {
if confirm {
main_dispatch.set(MainState::default());
ct_dispatch.set(CurrentTransfer::default());
}
}
})
}

View File

@ -0,0 +1,105 @@
#![allow(non_snake_case)]
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
HtmlDialogElement, HtmlFormElement, HtmlOptionElement, HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CsvExportType, MainState};
use super::create_close_button;
pub fn toggle_in_transfer_hashes_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
}
pub fn toggle_volume_heatmap_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.volume_heatmap ^= true;
})
})
}
pub fn change_csv_export_type_callback(
main_dispatch: Dispatch<MainState>,
) -> Callback<web_sys::MouseEvent> {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
modal.set_text_content(Some("CSV Export Type"));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
modal.append_child(&close_button).unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let export_type = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for t in [CsvExportType::Normal, CsvExportType::EchoClient] {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&t.to_string());
option.set_text(&t.to_string());
option.set_selected(t == main_dispatch.get().preferences.csv_export_type);
export_type.append_child(&option).unwrap();
}
form.append_child(&export_type).unwrap();
let form_change_callback = {
let export_type = export_type.clone();
let main_dispatch = main_dispatch.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let current: CsvExportType = export_type.value().as_str().into();
main_dispatch.reduce_mut(|state| {
state.preferences.csv_export_type = current;
});
})};
form.set_onchange(Some(form_change_callback.as_ref().unchecked_ref()));
form_change_callback.forget();
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
}

View File

@ -0,0 +1,19 @@
#![allow(non_snake_case)]
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::HtmlElement;
use yew::prelude::*;
pub fn create_close_button(close: &Closure<dyn FnMut(Event)>) -> HtmlElement {
let document = web_sys::window().unwrap().document().unwrap();
let close_button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
close_button.set_text_content(Some(""));
close_button.set_class_name("close_button");
close_button.set_onclick(Some(close.as_ref().unchecked_ref()));
close_button
}

View File

@ -0,0 +1,4 @@
pub mod main_window_callbacks;
pub mod new_plate_dialog_callbacks;
pub mod transfer_menu_callbacks;
pub mod tree_callbacks;

View File

@ -0,0 +1,61 @@
use yew::prelude::*;
use yewdux::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, FormData, HtmlFormElement};
use crate::components::states::MainState;
use plate_tool_lib::plate::*;
use plate_tool_lib::plate_instances::PlateInstance;
pub fn new_plate_callback(
dispatch: Dispatch<MainState>,
close_callback: Callback<()>,
) -> Callback<SubmitEvent> {
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
close_callback.emit(());
let target: Option<EventTarget> = e.target();
let form = target.and_then(|t| t.dyn_into::<HtmlFormElement>().ok());
if let Some(form) = form {
if let Ok(form_data) = FormData::new_with_form(&form) {
let name = form_data.get("new_plate_name").as_string().unwrap();
let format = match form_data.get("plate_format").as_string().unwrap().as_str() {
"6" => PlateFormat::W6,
"12" => PlateFormat::W12,
"24" => PlateFormat::W24,
"48" => PlateFormat::W48,
"96" => PlateFormat::W96,
"384" => PlateFormat::W384,
"1536" => PlateFormat::W1536,
"3456" => PlateFormat::W3456,
_ => unreachable!(),
};
if let Some(pt_string) = form_data.get("new_plate_type").as_string() {
let plate_type = match pt_string.as_str() {
"src" => PlateType::Source,
"dest" => PlateType::Destination,
_ => PlateType::Source,
};
dispatch.reduce_mut(|s| {
if plate_type == PlateType::Source {
s.add_source_plate(PlateInstance::new(PlateType::Source, format, name))
} else {
s.add_dest_plate(PlateInstance::new(
PlateType::Destination,
format,
name,
))
}
});
}
}
}
})
}
pub fn onclose(close_callback: Callback<()>) -> Callback<Event> {
Callback::from(move |_: Event| {
close_callback.emit(());
})
}

View File

@ -0,0 +1,245 @@
use std::rc::Rc;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlInputElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::transfer_menu::RegionDisplay;
use plate_tool_lib::{transfer::Transfer, transfer_region::Region, transfer_volume::TransferVolume};
use crate::components::states::{CurrentTransfer, MainState};
pub fn on_name_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if input.value() == "" {
return;
} // We do not want empty inputs!
ct_dispatch.reduce_mut(|state| {
state.transfer.name = input.value();
});
}
})
}
pub fn on_src_region_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<Event> {
Callback::from(move |e: Event| {
if matches!(
ct_dispatch.get().transfer.transfer_region.source_region,
Region::Custom(_)
) {
return; // Do nothing here!
}
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(rd) = RegionDisplay::try_from(input.value().to_uppercase()) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::from(&rd);
});
input.set_custom_validity("");
} else {
input.set_custom_validity("Invalid region.")
}
}
})
}
pub fn on_dest_region_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(rd) = RegionDisplay::try_from(input.value().to_uppercase()) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_region = Region::from(&rd);
});
input.set_custom_validity("");
} else {
input.set_custom_validity("Invalid region.")
}
}
})
}
pub fn on_source_interleave_x_change_callback(
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_source =
(num, state.transfer.transfer_region.interleave_source.1);
});
}
}
})
}
pub fn on_source_interleave_y_change_callback(
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_source =
(state.transfer.transfer_region.interleave_source.0, num);
});
}
}
})
}
pub fn on_dest_interleave_x_change_callback(
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_dest =
(num, state.transfer.transfer_region.interleave_dest.1);
});
}
}
})
}
pub fn on_dest_interleave_y_change_callback(
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<Event> {
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_dest =
(state.transfer.transfer_region.interleave_dest.0, num);
});
}
}
})
}
pub fn on_volume_change_callback(ct_dispatch: Dispatch<CurrentTransfer>) -> Callback<Event> {
Callback::from(move |e: Event| {
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.expect("Must have been emitted by input");
if let Ok(num) = input.value().parse::<f32>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.volume = TransferVolume::Single(num);
});
}
})
}
pub fn new_transfer_button_callback_callback(
main_dispatch: Dispatch<MainState>,
main_state: Rc<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |_: MouseEvent| {
main_dispatch.reduce_mut(|state| {
state.selected_transfer = Uuid::nil();
});
ct_dispatch.reduce_mut(|state| {
state.transfer = Transfer::default();
state.transfer.source_id = main_state.selected_source_plate;
state.transfer.dest_id = main_state.selected_dest_plate;
});
})
}
pub fn save_transfer_button_callback_callback(
main_dispatch: Dispatch<MainState>,
main_state: Rc<MainState>,
ct_state: Rc<CurrentTransfer>,
new_transfer_button_callback: Callback<MouseEvent>)
-> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
log::debug!("Button pressed");
if main_state.selected_transfer.is_nil() {
if let Some(spi) = main_state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == main_state.selected_source_plate)
{
if let Some(dpi) = main_state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
{
// Only mutable for volume assignment!
let mut new_transfer = Transfer::new(
spi.clone(),
dpi.clone(),
ct_state.transfer.transfer_region.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| {
state.transfers.push(new_transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
new_transfer_button_callback.emit(e); // If we just made a new transfer,
// then we should make another on
// save.
}
}
} else if let Some(index) = main_state
.transfers
.iter()
.position(|t| t.get_uuid() == main_state.selected_transfer)
{
main_dispatch.reduce_mut(|state| {
state.transfers[index] = ct_state.transfer.clone();
});
}
})
}
pub fn delete_transfer_button_callback(
main_state: Rc<MainState>,
main_dispatch: Dispatch<MainState>,
ct_state: Rc<CurrentTransfer>,
new_transfer_button_callback: Callback<MouseEvent>
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
if main_state.selected_transfer.is_nil() {
// Maybe reset transfer?
} else if let Some(index) = main_state
.transfers
.iter()
.position(|t| t.get_uuid() == ct_state.transfer.get_uuid())
{
main_dispatch.reduce_mut(|state| {
state.transfers.remove(index);
state.selected_transfer = Uuid::nil();
});
new_transfer_button_callback.emit(e); // We need a new transfer now
}
})
}

View File

@ -0,0 +1,74 @@
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlElement, HtmlInputElement, HtmlOptionElement, HtmlSelectElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::MainState;
use plate_tool_lib::plate::PlateFormat;
type NoParamsCallback = Box<dyn Fn(())>;
pub fn open_plate_info_callback(
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
plate_menu_id.set(Some(Uuid::from_u128(id)));
}
}
})
}
pub fn plate_info_close_callback(plate_menu_id: UseStateHandle<Option<Uuid>>) -> NoParamsCallback {
Box::new(move |_| {
plate_menu_id.set(None);
})
}
pub fn plate_info_delete_callback(
main_dispatch: Dispatch<MainState>,
plate_menu_id: UseStateHandle<Option<Uuid>>,
) -> NoParamsCallback {
Box::new(move |_| {
if let Some(id) = *plate_menu_id {
main_dispatch.reduce_mut(|state| {
state.del_plate(id);
});
}
})
}
pub fn rename_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changed name");
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.unwrap();
main_dispatch.reduce_mut(|state| state.rename_plate(id, &input.value()))
})
}
pub fn format_onchange(id: Uuid, main_dispatch: Dispatch<MainState>) -> Callback<Event> {
Callback::from(move |e: Event| {
log::debug!("Changing plate format");
let new_format: Option<PlateFormat> = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlSelectElement>()
.unwrap()
.selected_options()
.get_with_index(0)
.map(|el| el.dyn_into::<HtmlOptionElement>().unwrap())
.map(|opt_el| opt_el.value())
.and_then(|value| PlateFormat::try_from(value.as_str()).ok());
if let Some(format) = new_format {
main_dispatch.reduce_mut(|state| state.change_format(id, &format));
}
})
}

View File

@ -0,0 +1,217 @@
use plate_tool_lib::{plate::PlateType};
use uuid::Uuid;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{
HtmlDialogElement, HtmlElement, HtmlOptionElement,
HtmlSelectElement,
};
use yew::prelude::*;
use yewdux::prelude::*;
use std::rc::Rc;
use std::str::FromStr;
use crate::components::callbacks::main_window_callbacks::create_close_button;
use crate::components::states::MainState;
pub fn merge_button_callback(main_dispatch: Dispatch<MainState>, id: Uuid) -> Callback<MouseEvent> {
Callback::from(move |_| {
let dialog_opt = create_merge_dialog(main_dispatch.clone(), id);
if let Some(dialog) = dialog_opt {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let _ = body.append_child(&dialog);
dialog.set_open(true);
}
})
}
fn create_merge_dialog(main_dispatch: Dispatch<MainState>, id: Uuid) -> Option<HtmlDialogElement> {
let other_plates = get_all_plates_except(main_dispatch.clone(), id);
if other_plates.is_empty() {
return None;
}
let dialog = create_dialog();
let select = create_select_for_plates(&other_plates);
dialog.append_child(&select).unwrap();
let select_rc = Rc::new(select);
let ok_button = create_ok_button(main_dispatch.clone(), select_rc.clone(), id);
dialog.append_child(&ok_button).unwrap();
Some(dialog)
}
fn create_dialog() -> HtmlDialogElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let dialog = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
dialog.set_class_name("dialog shifted_dialog");
let onclose_callback = {
let dialog = dialog.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
dialog.remove();
})
};
dialog.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
let close_button = create_close_button(&onclose_callback);
onclose_callback.forget();
dialog.append_child(&close_button).unwrap();
let header = document.create_element("h2").unwrap();
header.set_inner_html("Merge Plates");
dialog.append_child(&header).unwrap();
dialog
}
fn create_ok_button(
main_dispatch: Dispatch<MainState>,
select: Rc<HtmlSelectElement>,
id: Uuid,
) -> HtmlElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let button = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap();
button.set_inner_html("Ok");
let callback = ok_button_callback(main_dispatch, select, id);
button.set_onclick(Some(callback.as_ref().unchecked_ref()));
callback.forget();
button
}
fn ok_button_callback(
main_dispatch: Dispatch<MainState>,
select: Rc<HtmlSelectElement>,
to_id: Uuid,
) -> Closure<dyn FnMut(Event)> {
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let selected = select
.selected_options()
.get_with_index(0)
.map(|el| el.dyn_into::<HtmlOptionElement>().unwrap())
.map(|opt_el| opt_el.value())
.and_then(|uuid_str| Uuid::from_str(&uuid_str).ok());
if let Some(from_id) = selected {
let plate_type = find_plate_type(main_dispatch.clone(), from_id).unwrap();
let binding = main_dispatch.get();
let merge_to = match plate_type {
PlateType::Source => binding.source_plates.iter().find(|x| x.get_uuid() == to_id).unwrap(),
PlateType::Destination => binding
.destination_plates
.iter()
.find(|x| x.get_uuid() == to_id).unwrap(),
};
main_dispatch.reduce_mut(|x| {
let merged_transfers = x.transfers.iter_mut().filter(|x| match plate_type {
PlateType::Source => x.source_id,
PlateType::Destination => x.dest_id
} == from_id);
for transfer in merged_transfers {
match plate_type {
PlateType::Source => {
transfer.source_id = to_id;
transfer.transfer_region.source_plate = merge_to.plate;
},
PlateType::Destination => {
transfer.dest_id = to_id;
transfer.transfer_region.dest_plate = merge_to.plate;
}
}
}
match plate_type {
PlateType::Source => x.source_plates.retain(|y| y.get_uuid() != from_id),
PlateType::Destination => x.destination_plates.retain(|y| y.get_uuid() != from_id)
};
});
}
})
}
fn create_select_for_plates(infos: &Vec<(Uuid, String)>) -> HtmlSelectElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let select = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for info in infos {
let _ = select.append_child(&create_option_for_plate(info));
}
select
}
fn create_option_for_plate(info: &(Uuid, String)) -> HtmlOptionElement {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let opt = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
opt.set_value(&info.0.to_string());
opt.set_inner_text(&info.1);
opt
}
fn get_all_plates_except(main_dispatch: Dispatch<MainState>, id: Uuid) -> Vec<(Uuid, String)> {
let plate_type = find_plate_type(main_dispatch.clone(), id);
if plate_type.is_none() {
return Vec::new();
} // Return early if not found somehow
let plate_type = plate_type.unwrap();
let binding = main_dispatch.get();
let all_plates_iter = match plate_type {
PlateType::Source => binding.source_plates.iter(),
PlateType::Destination => binding.destination_plates.iter(),
};
all_plates_iter
.map(|x| (x.get_uuid(), x.name.to_string()))
.filter(|y| y.0 != id)
.collect()
}
fn find_plate_type(main_dispatch: Dispatch<MainState>, id: Uuid) -> Option<PlateType> {
if main_dispatch
.get()
.source_plates
.iter()
.any(|x| x.get_uuid() == id)
{
Some(PlateType::Source)
} else if main_dispatch
.get()
.destination_plates
.iter()
.any(|x| x.get_uuid() == id)
{
return Some(PlateType::Destination);
} else {
return None;
}
}

View File

@ -0,0 +1,9 @@
mod select_callbacks;
mod info_dialog_callbacks;
mod merge_callbacks;
pub use select_callbacks::*;
pub use info_dialog_callbacks::*;
pub use merge_callbacks::*;

View File

@ -0,0 +1,83 @@
use std::rc::Rc;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use plate_tool_lib::transfer_region::Region;
pub fn source_plate_select_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::default();
state.transfer.transfer_region.dest_region = Region::default();
});
main_dispatch.reduce_mut(|state| {
state.selected_source_plate = Uuid::from_u128(id);
state.selected_transfer = Uuid::nil();
});
}
}
})
}
pub fn destination_plate_select_callback(
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::default();
state.transfer.transfer_region.dest_region = Region::default();
});
main_dispatch.reduce_mut(|state| {
state.selected_dest_plate = Uuid::from_u128(id);
state.selected_transfer = Uuid::nil();
});
}
}
})
}
pub fn transfer_select_callback(
main_state: Rc<MainState>,
main_dispatch: Dispatch<MainState>,
ct_dispatch: Dispatch<CurrentTransfer>,
) -> Callback<MouseEvent> {
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
let id = Uuid::from_u128(id);
if let Some(transfer) = main_state
.transfers
.iter()
.find(|transfer| transfer.get_uuid() == id)
{
main_dispatch.reduce_mut(|state| {
state.selected_source_plate = transfer.source_id;
state.selected_dest_plate = transfer.dest_id;
state.selected_transfer = id;
});
ct_dispatch.reduce_mut(|state| {
state.transfer = transfer.clone();
});
}
}
}
})
}

View File

@ -0,0 +1,133 @@
#![allow(non_snake_case)]
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::new_plate_dialog::{NewPlateDialog, NewPlateDialogType};
use crate::components::plates::plate_container::PlateContainer;
use crate::components::states::{CurrentTransfer, MainState};
use crate::components::transfer_menu::TransferMenu;
use crate::components::tree::Tree;
use plate_tool_lib::plate_instances::PlateInstance;
use crate::components::callbacks::main_window_callbacks;
#[function_component]
pub fn MainWindow() -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (_, ct_dispatch) = use_store::<CurrentTransfer>();
let source_plate_instance: Option<PlateInstance> = main_state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == main_state.selected_source_plate)
.cloned();
if let Some(spi) = source_plate_instance.clone() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_plate = spi.plate;
});
}
let destination_plate_instance: Option<PlateInstance> = main_state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
.cloned();
if let Some(dpi) = destination_plate_instance.clone() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_plate = dpi.plate;
});
}
let toggle_in_transfer_hashes_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::toggle_in_transfer_hashes_callback(main_dispatch)
};
let toggle_volume_heatmap_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::toggle_volume_heatmap_callback(main_dispatch)
};
let change_csv_export_type_callback = {
let main_dispatch = main_dispatch.clone();
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_callback =
main_window_callbacks::new_plate_dialog_callback(new_plate_dialog_is_open.clone());
let open_new_plate_dialog_callback =
main_window_callbacks::open_new_plate_dialog_callback(new_plate_dialog_is_open.clone());
let new_button_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::new_button_callback(main_dispatch, ct_dispatch)
};
let export_csv_button_callback = {
let main_state = main_state.clone();
main_window_callbacks::export_csv_button_callback(main_state)
};
let export_json_button_callback =
{ main_window_callbacks::export_json_button_callback(main_state) };
let import_json_button_callback = {
let main_dispatch = main_dispatch.clone();
main_window_callbacks::import_json_button_callback(main_dispatch)
};
let import_transfer_csv_callback =
main_window_callbacks::import_transfer_csv_callback(main_dispatch.clone());
html! {
<>
<div class="upper_menu">
<div class="dropdown">
<button>{"File"}</button>
<button onclick={new_button_callback}>{"New"}</button>
<div class="dropdown-sub">
<button>{"Export"}</button>
<div>
<button onclick={export_csv_button_callback}>{"Export as CSV"}</button>
<button onclick={export_json_button_callback}>{"Export as JSON"}</button>
</div>
</div>
<div class="dropdown-sub">
<button>{"Import"}</button>
<div>
<button onclick={import_json_button_callback}>{"Import from JSON"}</button>
<button onclick={import_transfer_csv_callback}>{"Import Transfer from CSV"}</button>
</div>
</div>
</div>
<div class="dropdown">
<button>{"Options"}</button>
<div class="dropdown-sub">
<button>{"Styles"}</button>
<div>
<button onclick={toggle_in_transfer_hashes_callback}>{"Toggle transfer hashes"}</button>
<button onclick={toggle_volume_heatmap_callback}>{"Toggle volume heatmap"}</button>
</div>
</div>
<div class="dropdown-sub">
<button>{"Export"}</button>
<div>
<button onclick={change_csv_export_type_callback}>{"Change CSV export type"}</button>
</div>
</div>
</div>
</div>
<div class="main_container">
<Tree open_new_plate_callback={open_new_plate_dialog_callback}/>
<TransferMenu />
<PlateContainer source_dims={source_plate_instance}
destination_dims={destination_plate_instance}/>
if {new_plate_dialog_is_open.is_some()} {
<NewPlateDialog close_callback={new_plate_dialog_callback} dialog_type={new_plate_dialog_is_open.unwrap()}/>
}
</div>
</>
}
}

View File

@ -4,3 +4,4 @@ pub mod plates;
pub mod states; pub mod states;
pub mod transfer_menu; pub mod transfer_menu;
pub mod tree; pub mod tree;
mod callbacks;

View File

@ -0,0 +1,108 @@
use yew::prelude::*;
use yewdux::prelude::*;
use web_sys::HtmlDialogElement;
use crate::components::states::MainState;
use crate::components::callbacks::new_plate_dialog_callbacks;
#[derive(PartialEq, Properties)]
pub struct NewPlateDialogProps {
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]
pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
let (_, dispatch) = use_store::<MainState>();
let new_plate_callback = {
let close_callback = props.close_callback.clone();
new_plate_dialog_callbacks::new_plate_callback(dispatch, close_callback)
};
let onclose = {
let close_callback = props.close_callback.clone();
new_plate_dialog_callbacks::onclose(close_callback)
};
// This whole section is optional, only if you want the backdrop
let dialog_ref = use_node_ref();
{
let dialog_ref = dialog_ref.clone();
use_effect_with_deps(
|dialog_ref| {
dialog_ref
.cast::<HtmlDialogElement>()
.unwrap()
.show_modal()
.ok();
},
dialog_ref,
);
}
let header_interior_text = match props.dialog_type {
NewPlateDialogType::Both => "",
NewPlateDialogType::SourceOnly => " source",
NewPlateDialogType::DestinationOnly => " destination",
};
html! {
<dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}>
<h2>{format!("Create a{} plate:", header_interior_text)}</h2>
<form onsubmit={new_plate_callback}>
<input type="text" name="new_plate_name" placeholder="Name"/>
<select name="plate_format">
<option value="6">{"6"}</option>
<option value="12">{"12"}</option>
<option value="24">{"24"}</option>
<option value="48">{"48"}</option>
<option value="96" selected={true}>{"96"}</option>
<option value="384">{"384"}</option>
<option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option>
</select>
{ plate_type_selector(props.dialog_type) }
<input type="submit" name="new_plate_button" value="Create" />
</form>
<form class="modal_close" method="dialog"><button /></form>
</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

@ -0,0 +1,5 @@
pub mod plate_container;
mod util;
mod plate_callbacks;
mod plate_data;
mod plate;

View File

@ -0,0 +1,347 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use plate_tool_lib::transfer_volume::TransferVolume;
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use plate_tool_lib::plate::PlateType;
use plate_tool_lib::transfer::Transfer;
use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::Well;
// Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes;
const PALETTE: super::util::ColorPalette = Palettes::RAINBOW;
use plate_tool_lib::util::num_to_letters;
use super::plate_data::*;
use super::plate_callbacks;
#[function_component]
pub fn Plate(props: &PlateProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let m_start_handle: MStartHandle = use_state_eq(|| None);
let m_end_handle: MEndHandle = use_state_eq(|| None);
let m_stat_handle: MStatHandle = use_state_eq(|| false);
if !(*m_stat_handle) {
let region = match props.ptype {
PlateType::Source => ct_state.transfer.transfer_region.source_region.clone(),
PlateType::Destination => ct_state.transfer.transfer_region.dest_region.clone(),
};
let (pt1, pt2) = match region {
Region::Point(Well {row: x, col: y }) => ((x, y), (x, y)),
Region::Rect(c1, c2) => ((c1.row, c1.col ), (c2.row, c2.col)),
Region::Custom(_) => ((0, 0), (0, 0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));
}
let tooltip_map: HashMap<Well, Vec<&Transfer>>;
let volume_map: HashMap<Well, f32>;
let volume_max: f32;
{
let transfers = main_state.transfers.iter().filter(|t| match props.ptype {
PlateType::Source => t.source_id == props.source_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 volume_map_temp: HashMap<Well, f32> = HashMap::new();
let mut volume_max_temp: f32 = f32::NEG_INFINITY;
for transfer in transfers {
let wells = match props.ptype {
PlateType::Source => transfer.transfer_region.get_source_wells(),
PlateType::Destination => transfer.transfer_region.get_destination_wells(),
};
for well in wells {
if let Some(val) = tooltip_map_temp.get_mut(&well) {
val.push(transfer);
} else {
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) {
*val += temp_volume * usage;
} else {
volume_map_temp.insert(well, temp_volume * usage);
}
volume_max_temp = f32::max(volume_max_temp, *volume_map_temp.get(&well).expect("Just added"));
}
}
tooltip_map = tooltip_map_temp;
volume_map = volume_map_temp;
volume_max = volume_max_temp;
};
let wells = match props.ptype {
PlateType::Source => ct_state.transfer.transfer_region.get_source_wells(),
PlateType::Destination => ct_state.transfer.transfer_region.get_destination_wells(),
};
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter().map(|x| x.id).collect();
ids.sort_unstable();
ids
};
let mouse_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
let m_stat_handle = m_stat_handle.clone();
plate_callbacks::mouse_callback(m_start_handle, m_end_handle, m_stat_handle)
};
let mouseup_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
plate_callbacks::mouseup_callback(m_start_handle, m_end_handle, m_stat_handle, ct_dispatch, props.ptype)
};
let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_src()");
});
let width = match props.ptype {
PlateType::Source => props.source_plate.plate.size().1,
PlateType::Destination => props.destination_plate.plate.size().1,
};
let height = match props.ptype {
PlateType::Source => props.source_plate.plate.size().0,
PlateType::Destination => props.destination_plate.plate.size().0,
};
let pformat = match props.ptype {
PlateType::Source => props.source_plate.plate.plate_format,
PlateType::Destination => props.destination_plate.plate.plate_format,
};
let column_header = {
let headers = (1..=width)
.map(|j| {
html! {<th>
{format!("{:0>2}", j)}
</th>}
})
.collect::<Html>();
html! {<tr><th />{ headers }</tr>}
};
let rows =
(1..=height)
.map(|i| {
let row_header = html! {<th>{num_to_letters(i)}</th>};
let row = (1..=width)
.map(|j| {
let color = {
if !main_state.preferences.volume_heatmap {
tooltip_map.get(&Well { row: i, col: j })
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
} else {
volume_map.get(&Well { row: i, col: j })
.map(|t| PALETTE.get_linear(*t as f64, volume_max as f64))
}
};
let title = {
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())
.collect::<Vec<_>>().join(", ")));
if let Some(val) = used_by {
out += &val;
}
let volume_sum = volume_map.get(&Well { row: i, col: j })
.map(|t| format!("Volume: {}", t));
if let Some(val) = volume_sum {
if !out.is_empty() { out += "\n" }
out += &val;
}
out
};
html! {
<PlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={wells.contains(&Well { row: i, col: j }) && main_state.preferences.in_transfer_hashes}
color={ color }
cell_height={props.cell_height}
title={title}
/>
}
})
.collect::<Html>();
html! {
<tr>
{ row_header }{ row }
</tr>
}
})
.collect::<Html>();
html! {
<div ondblclick={screenshot_callback}
class={classes!{match props.ptype {
PlateType::Source => "source_plate",
PlateType::Destination => "dest_plate",
},
"W".to_owned()+&pformat.to_string()}}>
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
}}
onmouseleave={move |e| {
mouseleave_callback.emit(e);
}}>
{ column_header }
{ rows }
</table>
</div>
}
}
#[derive(PartialEq, Properties)]
pub struct PlateCellProps {
i: u8,
j: u8,
selected: bool,
mouse: Callback<(u8, u8, MouseEventType)>,
in_transfer: Option<bool>,
color: Option<[f64; 3]>,
cell_height: f64,
title: Option<String>,
}
#[function_component]
fn PlateCell(props: &PlateCellProps) -> Html {
let selected_class = match props.selected {
true => Some("current_select"),
false => None,
};
let in_transfer_class = match props.in_transfer {
Some(true) => Some("in_transfer"),
_ => None,
};
let color = props.color.unwrap_or([255.0, 255.0, 255.0]);
let mouse = Callback::clone(&props.mouse);
let mouse2 = Callback::clone(&props.mouse);
let (i, j) = (props.i, props.j);
html! {
<td class={classes!("plate_cell", selected_class, in_transfer_class)}
style={format!("height: {}px;", props.cell_height)}
id={format!("color={:?}", props.color)}
onmousedown={move |_| {
mouse.emit((i,j, MouseEventType::Mousedown))
}}
onmouseenter={move |_| {
mouse2.emit((i,j, MouseEventType::Mouseenter))
}}>
<div class="plate_cell_inner"
style={format!("background: rgba({},{},{},1);", color[0], color[1], color[2])}
title={if let Some(text) = &props.title {
text.clone()
} else {"".to_string()}}/>
</td>
}
}
pub fn in_rect(corner1: Option<(u8, u8)>, corner2: Option<(u8, u8)>, pt: (u8, u8)) -> bool {
if let (Some(c1), Some(c2)) = (corner1, corner2) {
pt.0 <= u8::max(c1.0, c2.0)
&& pt.0 >= u8::min(c1.0, c2.0)
&& pt.1 <= u8::max(c1.1, c2.1)
&& pt.1 >= u8::min(c1.1, c2.1)
} else {
false
}
}
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)]
mod tests {
use wasm_bindgen_test::*;
use super::in_rect;
// in_rect tests
#[test]
#[wasm_bindgen_test]
fn test_in_rect1() {
// Test in center of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt = (5, 5);
assert!(in_rect(Some(c1), Some(c2), pt));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect2() {
// Test on top/bottom edges of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (1, 5);
let pt2 = (10, 5);
assert!(in_rect(Some(c1), Some(c2), pt1));
assert!(in_rect(Some(c1), Some(c2), pt2));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt1));
assert!(in_rect(Some(c2), Some(c1), pt2));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect3() {
// Test on left/right edges of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (5, 1);
let pt2 = (5, 10);
assert!(in_rect(Some(c1), Some(c2), pt1));
assert!(in_rect(Some(c1), Some(c2), pt2));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt1));
assert!(in_rect(Some(c2), Some(c1), pt2));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect4() {
// Test cases that should fail
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (0, 0);
let pt2 = (15, 15);
assert!(!in_rect(Some(c1), Some(c2), pt1));
assert!(!in_rect(Some(c1), Some(c2), pt2));
}
}

View File

@ -0,0 +1,55 @@
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::CurrentTransfer;
use plate_tool_lib::transfer_region::Region;
use plate_tool_lib::plate::PlateType;
// Color Palette for the Source Plates, can be changed here
use super::super::transfer_menu::RegionDisplay;
use super::plate_data::*;
pub fn mouse_callback(
m_start_handle: MStartHandle,
m_end_handle: MEndHandle,
m_stat_handle: MStatHandle,
) -> Callback<(u8, u8, MouseEventType)> {
Callback::from(move |(i, j, t)| match t {
MouseEventType::Mousedown => {
m_start_handle.set(Some((i, j)));
m_end_handle.set(Some((i, j)));
m_stat_handle.set(true);
}
MouseEventType::Mouseenter => {
if *m_stat_handle {
m_end_handle.set(Some((i, j)));
}
}
})
}
pub fn mouseup_callback(
m_start_handle: MStartHandle,
m_end_handle: MEndHandle,
m_stat_handle: MStatHandle,
ct_dispatch: Dispatch<CurrentTransfer>,
ptype: PlateType,
) -> Callback<MouseEvent> {
Callback::from(move |_: MouseEvent| {
m_stat_handle.set(false);
if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.1, ul.0, br.1, br.0)) {
ct_dispatch.reduce_mut(|state| {
match ptype {
PlateType::Source => state.transfer.transfer_region.source_region = Region::from(&rd),
PlateType::Destination => state.transfer.transfer_region.dest_region = Region::from(&rd)
};
});
}
}
}
})
}

View File

@ -3,10 +3,14 @@ use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use yew::prelude::*; use yew::prelude::*;
use crate::data::plate_instances::PlateInstance; use plate_tool_lib::plate::PlateType;
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 crate::components::states::MainState;
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct PlateContainerProps { pub struct PlateContainerProps {
@ -16,6 +20,8 @@ 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()
@ -47,19 +53,28 @@ 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">
<h2>{spi.name.clone()}</h2> <h2>{spi.name.clone()}</h2>
<SourcePlate source_plate={spi.clone()} destination_plate={dpi.clone()} <h2>{"Source"}</h2>
cell_height={cell_height}/> <Plate source_plate={spi.clone()} destination_plate={dpi.clone()}
cell_height={cell_height} ptype={PlateType::Source}/>
</div> </div>
<div class="plate_container--destination"> <div class="plate_container--destination">
<h2>{dpi.name.clone()}</h2> <h2>{dpi.name.clone()}</h2>
<DestinationPlate source_plate={spi.clone()} destination_plate={dpi.clone()} <h2>{"Destination"}</h2>
cell_height={cell_height}/> <Plate source_plate={spi.clone()} destination_plate={dpi.clone()}
cell_height={cell_height} ptype={PlateType::Destination}/>
</div> </div>
} else { } else {
<h2>{"No Destination Plate Selected"}</h2> <h2>{"No Destination Plate Selected"}</h2>

View File

@ -0,0 +1,23 @@
use yew::prelude::*;
use plate_tool_lib::plate_instances::PlateInstance;
use plate_tool_lib::plate::PlateType;
#[derive(PartialEq, Properties)]
pub struct PlateProps {
pub source_plate: PlateInstance,
pub destination_plate: PlateInstance,
pub cell_height: f64,
pub ptype: PlateType,
}
pub type MStartHandle = UseStateHandle<Option<(u8, u8)>>;
pub type MEndHandle = UseStateHandle<Option<(u8, u8)>>;
pub type MStatHandle = UseStateHandle<bool>;
#[derive(Debug)]
pub enum MouseEventType {
Mousedown,
Mouseenter,
}

View File

@ -2,10 +2,6 @@
// https://iquilezles.org/articles/palettes/ // https://iquilezles.org/articles/palettes/
// http://dev.thi.ng/gradients/ // http://dev.thi.ng/gradients/
use rand::prelude::*;
use rand::rngs::SmallRng;
use lazy_static::lazy_static;
#[derive(Clone, Copy, PartialEq, Debug)] #[derive(Clone, Copy, PartialEq, Debug)]
pub struct ColorPalette { pub struct ColorPalette {
a: [f64; 3], a: [f64; 3],
@ -30,7 +26,8 @@ impl ColorPalette {
] ]
} }
pub fn _get_u8(&self, t: u8) -> [f64; 3] { #[allow(dead_code)] // Preserve old implementation for reference
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)
} }
@ -41,17 +38,22 @@ impl ColorPalette {
// self.get(r.gen_range(0.0..1.0f64)) // self.get(r.gen_range(0.0..1.0f64))
// } // }
pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &Vec<uuid::Uuid>) pub fn get_ordered(&self, t: uuid::Uuid, ordered_uuids: &[uuid::Uuid])
-> [f64; 3] { -> [f64; 3] {
let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1; let index = ordered_uuids.iter().position(|&x| x == t).expect("uuid must be in list of uuids") + 1;
return self.get(Self::space_evenly(index)) self.get(Self::space_evenly(index))
}
pub fn get_linear(&self, t: f64, max: f64) -> [f64; 3] {
let scaled = t / max;
self.get(scaled)
} }
fn space_evenly(x: usize) -> f64 { fn space_evenly(x: usize) -> f64 {
let e: usize = (x.ilog2() + 1) as usize; let e: usize = (x.ilog2() + 1) as usize;
let d: usize = (2usize.pow(e as u32)) as usize; let d: usize = 2usize.pow(e as u32);
let n: usize = (2*x + 1) % d; let n: usize = (2*x + 1) % d;
return (n as f64) / (d as f64); (n as f64) / (d as f64)
} }
} }

View File

@ -2,9 +2,9 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use yewdux::{prelude::*, storage}; use yewdux::{prelude::*, storage};
use crate::data::plate::*; use plate_tool_lib::plate::*;
use crate::data::plate_instances::PlateInstance; use plate_tool_lib::plate_instances::PlateInstance;
use crate::data::transfer::Transfer; use plate_tool_lib::transfer::Transfer;
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Store)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Store)]
#[store(storage = "session")] #[store(storage = "session")]
@ -15,12 +15,44 @@ pub struct CurrentTransfer {
#[derive(PartialEq, Clone, Copy, Serialize, Deserialize)] #[derive(PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct Preferences { pub struct Preferences {
#[serde(default)]
pub in_transfer_hashes: bool, pub in_transfer_hashes: bool,
#[serde(default)]
pub volume_heatmap: bool,
#[serde(default)]
pub csv_export_type: CsvExportType,
} }
impl Default for Preferences { impl Default for Preferences {
fn default() -> Self { fn default() -> Self {
Self { in_transfer_hashes: true } Self { in_transfer_hashes: true, volume_heatmap: false, csv_export_type: CsvExportType::Normal }
}
}
#[derive(PartialEq, Clone, Copy, Serialize, Deserialize, Default)]
pub enum CsvExportType {
#[default]
Normal,
EchoClient,
}
impl std::fmt::Display for CsvExportType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CsvExportType::Normal => write!(f, "Normal"),
CsvExportType::EchoClient => write!(f, "Echo Client"),
}
}
}
impl From<&str> for CsvExportType {
fn from(value: &str) -> Self {
match value.trim().to_lowercase().as_str() {
"normal" => CsvExportType::Normal,
"echo client" => CsvExportType::EchoClient,
_ => CsvExportType::default(),
}
} }
} }
@ -108,4 +140,13 @@ impl MainState {
self.destination_plates[index].change_name(new_name.to_string()); self.destination_plates[index].change_name(new_name.to_string());
} }
} }
pub fn change_format(&mut self, id: Uuid, new_format: &PlateFormat) {
if let Some(index) = self.source_plates.iter().position(|spi| spi.get_uuid() == id) {
self.source_plates[index].change_format(new_format);
}
if let Some(index) = self.destination_plates.iter().position(|dpi| dpi.get_uuid() == id) {
self.destination_plates[index].change_format(new_format);
}
}
} }

View File

@ -0,0 +1,305 @@
#![allow(non_snake_case)]
use lazy_static::lazy_static;
use plate_tool_lib::Well;
use regex::Regex;
use serde::{Deserialize, Serialize};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::callbacks::transfer_menu_callbacks;
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 super::states::{CurrentTransfer, MainState};
#[function_component]
pub fn TransferMenu() -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let on_name_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_name_change_callback(ct_dispatch)
};
let on_src_region_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_src_region_change_callback(ct_dispatch)
};
let on_dest_region_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_dest_region_change_callback(ct_dispatch)
};
let on_source_interleave_x_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_source_interleave_x_change_callback(ct_dispatch)
};
let on_source_interleave_y_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_source_interleave_y_change_callback(ct_dispatch)
};
let on_dest_interleave_x_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_dest_interleave_x_change_callback(ct_dispatch)
};
let on_dest_interleave_y_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_dest_interleave_y_change_callback(ct_dispatch)
};
let on_volume_change = {
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::on_volume_change_callback(ct_dispatch)
};
let new_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
let ct_dispatch = ct_dispatch.clone();
transfer_menu_callbacks::new_transfer_button_callback_callback(
main_dispatch,
main_state,
ct_dispatch,
)
};
let save_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
let ct_state = ct_state.clone();
let new_transfer_button_callback = new_transfer_button_callback.clone();
transfer_menu_callbacks::save_transfer_button_callback_callback(
main_dispatch,
main_state,
ct_state,
new_transfer_button_callback,
)
};
let delete_transfer_button_callback = {
let main_state = main_state.clone();
let main_dispatch = main_dispatch.clone();
let ct_state = ct_state.clone();
let new_transfer_button_callback = new_transfer_button_callback.clone();
transfer_menu_callbacks::delete_transfer_button_callback(
main_state,
main_dispatch,
ct_state,
new_transfer_button_callback,
)
};
html! {
<div class="transfer_menu">
<form>
<div>
<label for="name"><h3>{"Name:"}</h3></label>
<input type="text" name="name"
onchange={on_name_change}
value={ct_state.transfer.name.clone()}/>
</div>
// Anything below here is not rendered when a Custom transfer is selected
if !matches!(&ct_state.transfer.transfer_region.source_region, Region::Custom(_)) {
<div>
<label for="src_region"><h3>{"Source Region:"}</h3></label>
<input type="text" name="src_region"
onchange={on_src_region_change}
value={RegionDisplay::from(&ct_state.transfer.transfer_region.source_region).text}/>
</div>
<div>
<label for="dest_region"><h3>{"Destination Region:"}</h3></label>
<input type="text" name="dest_region"
onchange={on_dest_region_change}
value={RegionDisplay::from(&ct_state.transfer.transfer_region.dest_region).text}/>
</div>
<div>
<h3>{"Source Interleave "}</h3>
<label for="source_interleave_x">{"Row:"}</label>
<input type="number" name="source_interleave_x"
onchange={on_source_interleave_x_change}
value={ct_state.transfer.transfer_region.interleave_source.0.to_string()}/>
<label for="source_interleave_y">{"Col:"}</label>
<input type="number" name="source_interleave_y"
onchange={on_source_interleave_y_change}
value={ct_state.transfer.transfer_region.interleave_source.1.to_string()}/>
</div>
<div>
<h3>{"Destination Interleave "}</h3>
<label for="dest_interleave_x">{"Row:"}</label>
<input type="number" name="dest_interleave_x"
onchange={on_dest_interleave_x_change}
value={ct_state.transfer.transfer_region.interleave_dest.0.to_string()}/>
<label for="dest_interleave_y">{"Col:"}</label>
<input type="number" name="dest_interleave_y"
onchange={on_dest_interleave_y_change}
value={ct_state.transfer.transfer_region.interleave_dest.1.to_string()}/>
</div>
<div>
<label for="volume"><h3>{"Volume"}</h3></label>
<input type="number" name="volume" class="volume_input"
min="0" step="0.1"
onchange={on_volume_change}
value={match ct_state.transfer.volume {
TransferVolume::Single(x) => x.to_string(),
_ => unreachable!(),
}}/>
</div>
}
<div id="controls">
<input type="button" name="new_transfer" onclick={new_transfer_button_callback}
value={"New"} />
<input type="button" name="save_transfer" onclick={save_transfer_button_callback}
value={"Save"} />
<input type="button" name="delete_transfer" onclick={delete_transfer_button_callback}
value={"Delete"} />
</div>
</form>
</div>
}
}
#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
pub struct RegionDisplay {
pub text: String,
pub col_start: u8,
pub row_start: u8,
pub col_end: u8,
pub row_end: u8,
}
impl TryFrom<String> for RegionDisplay {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(&value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?;
let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?;
let row_start: u8 = captures[2]
.parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
Ok(RegionDisplay {
text: value,
col_start,
row_start,
col_end,
row_end,
})
} else {
Err("Regex match failed")
}
}
}
impl TryFrom<&str> for RegionDisplay {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?;
let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?;
let row_start: u8 = captures[2]
.parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
Ok(RegionDisplay {
text: value.to_string(),
col_start,
row_start,
col_end,
row_end,
})
} else {
Err("Regex match failed")
}
}
}
impl From<&Region> for RegionDisplay {
fn from(value: &Region) -> Self {
match *value {
Region::Point(Well { row, col }) => {
RegionDisplay::try_from((col, row, col, row)).ok().unwrap()
}
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.row, c1.col, c2.row, c2.col))
.ok()
.unwrap(),
Region::Custom(_) => RegionDisplay {
text: "CUSTOM".to_string(),
col_start: 0,
row_start: 0,
col_end: 0,
row_end: 0,
},
}
}
}
impl From<&RegionDisplay> for Region {
fn from(value: &RegionDisplay) -> Self {
if value.col_start == value.col_end && value.row_start == value.row_end {
Region::Point(Well { row: value.row_start, col: value.col_start })
} else {
Region::Rect(
Well { row: value.row_start, col: value.col_start },
Well { row: value.row_end, col: value.col_end },
)
}
}
}
impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay {
type Error = &'static str;
fn try_from(value: (u8, u8, u8, u8)) -> Result<Self, Self::Error> {
// (Column Start, Row Start, Column End, Row End)
// This can only possibly fail if one of the coordinates is zero...
let cs = num_to_letters(value.0).ok_or("Column start failed to parse")?;
let ce = num_to_letters(value.2).ok_or("Column end failed to parse")?;
Ok(RegionDisplay {
text: format!("{}{}:{}{}", cs, value.1, ce, value.3),
col_start: value.0,
row_start: value.1,
col_end: value.2,
row_end: value.3,
})
}
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::*;
#[test]
#[wasm_bindgen_test]
fn test_try_from_string_for_regiondisplay() {
let desired = RegionDisplay {
text: "A1:E5".to_string(),
row_start: 1,
row_end: 5,
col_start: 1,
col_end: 5,
};
assert_eq!(desired, "A1:E5".to_string().try_into().unwrap());
}
}

View File

@ -1,17 +1,19 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
use plate_tool_lib::plate::PlateFormat;
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlDialogElement, HtmlElement, HtmlInputElement}; use web_sys::{HtmlDialogElement};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::*; use yewdux::prelude::*;
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};
use crate::data::transfer_region::Region;
#[derive(PartialEq, Properties)] #[derive(PartialEq, Properties)]
pub struct TreeProps { pub struct TreeProps {
pub open_new_plate_callback: Callback<()>, pub open_new_plate_callback: Callback<NewPlateDialogType>,
} }
#[function_component] #[function_component]
@ -22,102 +24,33 @@ pub fn Tree(props: &TreeProps) -> Html {
let open_plate_info_callback = { let open_plate_info_callback = {
let plate_menu_id = plate_modal_id.clone(); let plate_menu_id = plate_modal_id.clone();
Callback::from(move |e: MouseEvent| { tree_callbacks::open_plate_info_callback(plate_menu_id)
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
plate_menu_id.set(Some(Uuid::from_u128(id)));
}
}
})
}; };
let plate_info_close_callback = { let plate_info_close_callback = {
let plate_menu_id = plate_modal_id.clone(); let plate_menu_id = plate_modal_id.clone();
Callback::from(move |_| { tree_callbacks::plate_info_close_callback(plate_menu_id)
plate_menu_id.set(None);
})
}; };
let plate_info_delete_callback = { let plate_info_delete_callback = {
let dispatch = main_dispatch.clone(); let main_dispatch = main_dispatch.clone();
let plate_menu_id = plate_modal_id.clone(); let plate_menu_id = plate_modal_id.clone();
Callback::from(move |_| { tree_callbacks::plate_info_delete_callback(main_dispatch, plate_menu_id)
if let Some(id) = *plate_menu_id {
dispatch.reduce_mut(|state| {
state.del_plate(id);
});
}
})
}; };
let source_plate_select_callback = { let source_plate_select_callback = {
let main_dispatch = main_dispatch.clone(); let main_dispatch = main_dispatch.clone();
let ct_dispatch = ct_dispatch.clone(); let ct_dispatch = ct_dispatch.clone();
tree_callbacks::source_plate_select_callback(main_dispatch, ct_dispatch)
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::default();
state.transfer.transfer_region.dest_region = Region::default();
});
main_dispatch.reduce_mut(|state| {
state.selected_source_plate = Uuid::from_u128(id);
state.selected_transfer = Uuid::nil();
});
}
}
})
}; };
let destination_plate_select_callback = { let destination_plate_select_callback = {
let main_dispatch = main_dispatch.clone(); let main_dispatch = main_dispatch.clone();
let ct_dispatch = ct_dispatch.clone(); let ct_dispatch = ct_dispatch.clone();
tree_callbacks::destination_plate_select_callback(main_dispatch, ct_dispatch)
Callback::from(move |e: MouseEvent| {
let target: Option<EventTarget> = e.target();
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::default();
state.transfer.transfer_region.dest_region = Region::default();
});
main_dispatch.reduce_mut(|state| {
state.selected_dest_plate = Uuid::from_u128(id);
state.selected_transfer = Uuid::nil();
});
}
}
})
}; };
let transfer_select_callback = { let transfer_select_callback = {
let main_state = main_state.clone(); let main_state = main_state.clone();
let main_dispatch = main_dispatch.clone();
Callback::from(move |e: MouseEvent| { let ct_dispatch = ct_dispatch.clone();
let target: Option<EventTarget> = e.target(); tree_callbacks::transfer_select_callback(main_state, main_dispatch, ct_dispatch)
let li = target.and_then(|t| t.dyn_into::<HtmlElement>().ok());
if let Some(li) = li {
if let Ok(id) = li.id().as_str().parse::<u128>() {
let id = Uuid::from_u128(id);
if let Some(transfer) = main_state
.transfers
.iter()
.find(|transfer| transfer.get_uuid() == id)
{
main_dispatch.reduce_mut(|state| {
state.selected_source_plate = transfer.source_id;
state.selected_dest_plate = transfer.dest_id;
state.selected_transfer = id;
});
ct_dispatch.reduce_mut(|state| {
state.transfer = transfer.clone();
});
}
}
}
})
}; };
let source_plates = main_state let source_plates = main_state
@ -166,13 +99,25 @@ 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>
@ -188,14 +133,6 @@ 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>
} }
} }
@ -226,24 +163,36 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
Some(plate) => plate.name.clone(), Some(plate) => plate.name.clone(),
None => "Not Found".to_string(), None => "Not Found".to_string(),
}; };
let plate_format = plate.map(|p| p.plate.plate_format);
let plate_formats = [PlateFormat::W6,
PlateFormat::W12,
PlateFormat::W24,
PlateFormat::W48,
PlateFormat::W96,
PlateFormat::W384,
PlateFormat::W1536,
PlateFormat::W3456];
let plate_format_options = plate_formats.iter().map(|v| {
let selected = Some(v) == plate_format.as_ref();
html! {
<option value={v.to_string()} selected={selected}>{ v.to_string() }</option>
}
});
let onclose = { let onclose = {
let dialog_close_callback = props.dialog_close_callback.clone(); let dialog_close_callback = props.dialog_close_callback.clone();
move |_| dialog_close_callback.emit(()) move |_| dialog_close_callback.emit(())
}; };
let rename_onchange = { let onclose_secondary = {
let id = props.id; let dialog_close_callback = props.dialog_close_callback.clone();
Callback::from(move |e: Event| { move |_| dialog_close_callback.emit(())
log::debug!("Changed name");
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.unwrap();
main_dispatch.reduce_mut(|state| state.rename_plate(id, &input.value()))
})
}; };
let rename_onchange = tree_callbacks::rename_onchange(props.id, main_dispatch.clone());
let format_onchange = tree_callbacks::format_onchange(props.id, main_dispatch.clone());
let delete_onclick = { let delete_onclick = {
let delete_button_callback = props.delete_button_callback.clone(); let delete_button_callback = props.delete_button_callback.clone();
let dialog_ref = dialog_ref.clone(); let dialog_ref = dialog_ref.clone();
@ -268,11 +217,22 @@ fn PlateInfoModal(props: &PlateInfoModalProps) -> Html {
); );
} }
let merge_button_callback = {
let main_dispatch = main_dispatch.clone();
tree_callbacks::merge_button_callback(main_dispatch, props.id)
};
html! { html! {
<dialog ref={dialog_ref} class="dialog" onclose={onclose}> <dialog ref={dialog_ref} class="dialog" onclose={onclose}>
<h2>{"Plate Info"}</h2> <h2>{"Plate Info"}</h2>
<h3>{"Name: "}<input type="text" value={plate_name} onchange={rename_onchange}/></h3> <h3>{"Name: "}<input type="text" value={plate_name} onchange={rename_onchange}/></h3>
<button onclick={delete_onclick}>{"Delete"}</button> <button onclick={delete_onclick}>{"Delete"}</button>
if let Some(_) = plate_format {
<select name="modal_plate_format" onchange={format_onchange}>
{ for plate_format_options }
</select>
}
<button onclick={merge_button_callback} onclick={onclose_secondary}>{"Merge"}</button>
<form class="modal_close" method="dialog"><button /></form> <form class="modal_close" method="dialog"><button /></form>
</dialog> </dialog>
} }

99
plate-tool-web/src/lib.rs Normal file
View File

@ -0,0 +1,99 @@
#![allow(non_snake_case)]
mod components;
use std::error::Error;
use components::main_window::MainWindow;
use components::states::MainState;
use plate_tool_lib::csv::*;
use yew::prelude::*;
#[cfg(debug_assertions)]
use plate_tool_lib::*;
#[function_component]
pub fn App() -> Html {
html! {
<MainWindow />
}
}
#[cfg(debug_assertions)]
pub fn plate_test() {
let source = plate::Plate::new(plate::PlateType::Source, plate::PlateFormat::W96);
let destination = plate::Plate::new(plate::PlateType::Destination, plate::PlateFormat::W384);
let transfer = transfer_region::TransferRegion {
source_plate: source,
source_region: transfer_region::Region::Rect(Well { row: 1, col: 1 }, Well { row: 2, col: 2 }),
dest_plate: destination,
dest_region: transfer_region::Region::Rect(Well { row: 2, col: 2 }, Well { row: 11, col: 11 }),
interleave_source: (1, 1),
interleave_dest: (3, 3),
};
println!("{}", transfer);
let sws = transfer.get_source_wells();
let m = transfer.calculate_map();
for w in sws {
println!("{:?} -> {:?}", w, m(w));
}
}
pub fn state_to_csv(state: &MainState) -> Result<String, Box<dyn Error>> {
match state.preferences.csv_export_type {
components::states::CsvExportType::Normal => state_to_csv_regular(state),
components::states::CsvExportType::EchoClient => state_to_csv_echo_client(state)
}
}
fn state_to_csv_regular(state: &MainState) -> Result<String, Box<dyn Error>> {
let mut records: Vec<TransferRecord> = Vec::new();
for transfer in &state.transfers {
let src_barcode = state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == transfer.source_id)
.ok_or("Found unpurged transfer")?;
let dest_barcode = state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == transfer.dest_id)
.ok_or("Found unpurged transfer")?;
records.append(&mut transfer_to_records(
transfer,
&src_barcode.name,
&dest_barcode.name,
))
}
records_to_csv(records)
}
fn state_to_csv_echo_client(state: &MainState) -> Result<String, Box<dyn Error>> {
let current_src = state.selected_source_plate;
let current_dst = state.selected_dest_plate;
let mut records: Vec<TransferRecord> = Vec::new();
for transfer in &state.transfers {
let src_barcode = state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == transfer.source_id)
.ok_or("Found unpurged transfer")?;
let dest_barcode = state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == transfer.dest_id)
.ok_or("Found unpurged transfer")?;
if src_barcode.get_uuid() == current_src && dest_barcode.get_uuid() == current_dst {
records.append(&mut transfer_to_records(
transfer,
&src_barcode.name,
&dest_barcode.name,
))
}
}
records_to_echo_client_csv(records)
}

View File

@ -1,4 +1,4 @@
use plate_tool::App; use plate_tool_web::App;
fn main() { fn main() {
wasm_logger::init(wasm_logger::Config::default()); wasm_logger::init(wasm_logger::Config::default());

View File

@ -1,528 +0,0 @@
#![allow(non_snake_case)]
use std::collections::HashSet;
use js_sys::Array;
use lazy_static::lazy_static;
use regex::Regex;
use wasm_bindgen::{prelude::*, JsCast, JsValue};
use web_sys::{
Blob, HtmlAnchorElement, HtmlButtonElement, HtmlDialogElement, HtmlFormElement,
HtmlInputElement, HtmlOptionElement, HtmlSelectElement, Url,
};
use yew::prelude::*;
use yewdux::prelude::*;
use super::new_plate_dialog::NewPlateDialog;
use super::plates::plate_container::PlateContainer;
use super::states::{CurrentTransfer, MainState};
use super::transfer_menu::{letters_to_num, RegionDisplay, TransferMenu};
use super::tree::Tree;
use crate::data::csv::state_to_csv;
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::{Region, TransferRegion};
#[function_component]
pub fn MainWindow() -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (_, ct_dispatch) = use_store::<CurrentTransfer>();
let source_plate_instance: Option<PlateInstance> = main_state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == main_state.selected_source_plate)
.cloned();
if let Some(spi) = source_plate_instance.clone() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_plate = spi.plate;
});
}
let destination_plate_instance: Option<PlateInstance> = main_state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
.cloned();
if let Some(dpi) = destination_plate_instance.clone() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_plate = dpi.plate;
});
}
let toggle_in_transfer_hashes_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
main_dispatch.reduce_mut(|state| {
state.preferences.in_transfer_hashes ^= true;
})
})
};
let new_plate_dialog_is_open = use_state_eq(|| false);
let new_plate_dialog_callback = {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Callback::from(move |_| {
new_plate_dialog_is_open.set(false);
})
};
let open_new_plate_dialog_callback = {
let new_plate_dialog_is_open = new_plate_dialog_is_open.clone();
Callback::from(move |_| {
new_plate_dialog_is_open.set(true);
})
};
let new_button_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let confirm =
window.confirm_with_message("This will reset all plates and transfers. Proceed?");
if let Ok(confirm) = confirm {
if confirm {
main_dispatch.set(MainState::default());
ct_dispatch.set(CurrentTransfer::default());
}
}
})
};
let export_csv_button_callback = {
let main_state = main_state.clone();
Callback::from(move |_| {
if main_state.transfers.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("No transfers to export.")
.unwrap();
return;
}
web_sys::window().unwrap().alert_with_message("CSV export is currently not importable. Export as JSON if you'd like to back up your work!").unwrap();
if let Ok(csv) = state_to_csv(&main_state) {
save_str(&csv, "transfers.csv");
}
})
};
let export_json_button_callback = {
Callback::from(move |_| {
if let Ok(json) = serde_json::to_string(&main_state) {
save_str(&json, "plate-tool-state.json");
} else {
web_sys::window()
.unwrap()
.alert_with_message("Failed to export.")
.unwrap();
}
})
};
let import_json_button_callback = {
let main_dispatch = main_dispatch.clone();
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
onclose_callback.forget();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".json");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) =
&fr1.result().ok().and_then(|v| v.as_string())
{
let ms = serde_json::from_str::<MainState>(value);
match ms {
Ok(ms) => main_dispatch.set(ms),
Err(e) => log::debug!("{:?}", e),
};
modal.close();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
};
let import_transfer_csv_callback = {
Callback::from(move |_| {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().unwrap();
let modal = document
.create_element("dialog")
.unwrap()
.dyn_into::<HtmlDialogElement>()
.unwrap();
modal.set_text_content(Some("Import File:"));
let onclose_callback = {
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
modal.remove();
})
};
modal.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
onclose_callback.forget();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let input = document
.create_element("input")
.unwrap()
.dyn_into::<HtmlInputElement>()
.unwrap();
input.set_type("file");
input.set_accept(".csv");
form.append_child(&input).unwrap();
let input_callback = {
let main_dispatch = main_dispatch.clone();
let modal = modal.clone();
Closure::<dyn FnMut(_)>::new(move |e: Event| {
if let Some(input) = e.current_target() {
let input = input
.dyn_into::<HtmlInputElement>()
.expect("We know this is an input.");
if let Some(files) = input.files() {
if let Some(file) = files.get(0) {
let fr = web_sys::FileReader::new().unwrap();
fr.read_as_text(&file).unwrap();
let fr1 = fr.clone(); // Clone to avoid outliving closure
let main_dispatch = main_dispatch.clone(); // Clone to satisfy FnMut
// trait
let modal = modal.clone();
let onload = Closure::<dyn FnMut(_)>::new(move |_: Event| {
if let Some(value) =
&fr1.result().ok().and_then(|v| v.as_string())
{
let mut rdr = csv::Reader::from_reader(value.as_bytes());
let mut records = Vec::new();
for record in
rdr.deserialize::<crate::data::csv::TransferRecord>()
{
match record {
Ok(r) => {
//log::debug!("{:?}", r);
records.push(r);
}
Err(e) => {
log::debug!("{:?}", e);
}
}
}
let mut sources: HashSet<String> = HashSet::new();
let mut destinations: HashSet<String> = HashSet::new();
for record in records.iter() {
sources.insert(record.source_plate.clone());
destinations.insert(record.destination_plate.clone());
}
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let form = document
.create_element("form")
.unwrap()
.dyn_into::<HtmlFormElement>()
.unwrap();
let from_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in sources {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&source);
option.set_text(&source);
from_source.append_child(&option).unwrap();
}
let to_source = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for source in &main_dispatch.get().source_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&source.name);
option.set_text(&source.name);
to_source.append_child(&option).unwrap();
}
let from_dest = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for dest in destinations {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&dest);
option.set_text(&dest);
from_dest.append_child(&option).unwrap();
}
let to_dest = document
.create_element("select")
.unwrap()
.dyn_into::<HtmlSelectElement>()
.unwrap();
for dest in &main_dispatch.get().destination_plates {
let option = document
.create_element("option")
.unwrap()
.dyn_into::<HtmlOptionElement>()
.unwrap();
option.set_value(&dest.name);
option.set_text(&dest.name);
to_dest.append_child(&option).unwrap();
}
let submit = document
.create_element("button")
.unwrap()
.dyn_into::<HtmlButtonElement>()
.unwrap();
submit.set_value("Submit");
submit.set_inner_text("Submit");
let submit_callback = {
let main_dispatch = main_dispatch.clone();
let from_source = from_source.clone();
let to_source = to_source.clone();
let from_dest = from_dest.clone();
let to_dest = to_dest.clone();
Closure::<dyn FnMut(_)>::new(move |_: Event| {
let from_source = from_source.value();
let to_source = to_source.value();
let from_dest = from_dest.value();
let to_dest = to_dest.value();
lazy_static! {
static ref REGEX: Regex =
Regex::new(r"([A-Z]+)(\d+)").unwrap();
}
let records: Vec<((u8, u8), (u8, u8))> = records
.iter()
.filter(|record| {
record.source_plate == from_source
})
.filter(|record| {
record.destination_plate == from_dest
})
.map(|record| {
let c1 = REGEX
.captures(&record.source_well)
.unwrap();
let c2 = REGEX
.captures(&record.destination_well)
.unwrap();
log::debug!("{} {}", &record.source_well, &record.destination_well);
log::debug!("{},{} {},{}", &c1[1], &c1[2], &c2[1], &c2[2]);
(
(
letters_to_num(&c1[1]).unwrap(),
c1[2].parse::<u8>().unwrap(),
),
(
letters_to_num(&c2[1]).unwrap(),
c2[2].parse::<u8>().unwrap(),
),
)
})
.collect();
let spi = main_dispatch
.get()
.source_plates
.iter()
.find(|src| src.name == to_source)
.unwrap()
.clone();
let dpi = main_dispatch
.get()
.destination_plates
.iter()
.find(|dest| dest.name == to_dest)
.unwrap()
.clone();
let custom_region = Region::new_custom(&records);
let transfer_region = TransferRegion {
source_region: custom_region.clone(),
dest_region: custom_region,
interleave_source: (1, 1),
interleave_dest: (1, 1),
source_plate: spi.plate,
dest_plate: dpi.plate,
};
let transfer = Transfer::new(
spi,
dpi,
transfer_region,
"Custom Transfer".to_string(),
);
main_dispatch.reduce_mut(|state| {
state.transfers.push(transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
})
};
submit.set_onclick(Some(
submit_callback.as_ref().unchecked_ref(),
));
submit_callback.forget();
form.append_child(&from_source).unwrap();
form.append_child(&to_source).unwrap();
form.append_child(&from_dest).unwrap();
form.append_child(&to_dest).unwrap();
modal.append_child(&submit).unwrap();
modal.append_child(&form).unwrap();
}
});
fr.set_onload(Some(onload.as_ref().unchecked_ref()));
onload.forget(); // Magic (don't touch)
}
}
}
})
};
input.set_onchange(Some(input_callback.as_ref().unchecked_ref()));
input_callback.forget(); // Magic straight from the docs, don't touch :(
modal.append_child(&form).unwrap();
body.append_child(&modal).unwrap();
modal.show_modal().unwrap();
})
};
html! {
<>
<div class="upper_menu">
<div class="dropdown">
<button>{"File"}</button>
<button onclick={new_button_callback}>{"New"}</button>
<div class="dropdown-sub">
<button>{"Export"}</button>
<div>
<button onclick={export_csv_button_callback}>{"Export as CSV"}</button>
<button onclick={export_json_button_callback}>{"Export as JSON"}</button>
</div>
</div>
<div class="dropdown-sub">
<button>{"Import"}</button>
<div>
<button onclick={import_json_button_callback}>{"Import from JSON"}</button>
<button onclick={import_transfer_csv_callback}>{"Import Transfer from CSV"}</button>
</div>
</div>
</div>
<div class="dropdown">
<button>{"Options"}</button>
<div class="dropdown-sub">
<button>{"Styles"}</button>
<div>
<button onclick={toggle_in_transfer_hashes_callback}>{"Toggle transfer hashes"}</button>
</div>
</div>
</div>
</div>
<div class="main_container">
<Tree open_new_plate_callback={open_new_plate_dialog_callback}/>
<TransferMenu />
<PlateContainer source_dims={source_plate_instance}
destination_dims={destination_plate_instance}/>
if {*new_plate_dialog_is_open} {
<NewPlateDialog close_callback={new_plate_dialog_callback}/>
}
</div>
</>
}
}
fn save_str(data: &str, name: &str) {
let blob =
Blob::new_with_str_sequence(&Array::from_iter(std::iter::once(JsValue::from_str(data))));
if let Ok(blob) = blob {
let url = Url::create_object_url_with_blob(&blob).expect("We have a blob, why not URL?");
// Beneath is the cool hack to download files
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let anchor = document
.create_element("a")
.unwrap()
.dyn_into::<HtmlAnchorElement>()
.unwrap();
anchor.set_download(name);
anchor.set_href(&url);
anchor.click();
}
}

View File

@ -1,123 +0,0 @@
use yew::prelude::*;
use yewdux::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, FormData, HtmlDialogElement, HtmlFormElement};
use crate::components::states::MainState;
use crate::data::plate::*;
use crate::data::plate_instances::PlateInstance;
#[derive(PartialEq, Properties)]
pub struct NewPlateDialogProps {
pub close_callback: Callback<()>,
}
#[function_component]
pub fn NewPlateDialog(props: &NewPlateDialogProps) -> Html {
let (_, dispatch) = use_store::<MainState>();
let new_plate_callback = {
let close_callback = props.close_callback.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
close_callback.emit(());
let target: Option<EventTarget> = e.target();
let form = target.and_then(|t| t.dyn_into::<HtmlFormElement>().ok());
if let Some(form) = form {
if let Ok(form_data) = FormData::new_with_form(&form) {
let name = form_data.get("new_plate_name").as_string().unwrap();
let format = match form_data.get("plate_format").as_string().unwrap().as_str() {
"6" => PlateFormat::W6,
"12" => PlateFormat::W12,
"24" => PlateFormat::W24,
"48" => PlateFormat::W48,
"96" => PlateFormat::W96,
"384" => PlateFormat::W384,
"1536" => PlateFormat::W1536,
"3456" => PlateFormat::W3456,
_ => unreachable!(),
};
if let Some(pt_string) = form_data.get("new_plate_type").as_string() {
let plate_type = match pt_string.as_str() {
"src" => PlateType::Source,
"dest" => PlateType::Destination,
_ => PlateType::Source,
};
dispatch.reduce_mut(|s| {
if plate_type == PlateType::Source {
s.add_source_plate(PlateInstance::new(
PlateType::Source,
format,
name,
))
} else {
s.add_dest_plate(PlateInstance::new(
PlateType::Destination,
format,
name,
))
}
});
}
}
}
})
};
let onclose = {
let close_callback = props.close_callback.clone();
Callback::from(move |_: Event| {
close_callback.emit(());
})
};
// This whole section is optional, only if you want the backdrop
let dialog_ref = use_node_ref();
{
let dialog_ref = dialog_ref.clone();
use_effect_with_deps(
|dialog_ref| {
dialog_ref
.cast::<HtmlDialogElement>()
.unwrap()
.show_modal()
.ok();
},
dialog_ref,
);
}
html! {
<dialog ref={dialog_ref} class="dialog new_plate_dialog" onclose={onclose}>
<h2>{"Create a plate:"}</h2>
<form onsubmit={new_plate_callback}>
<input type="text" name="new_plate_name" placeholder="Name"/>
<select name="plate_format">
<option value="6">{"6"}</option>
<option value="12">{"12"}</option>
<option value="24">{"24"}</option>
<option value="48">{"48"}</option>
<option value="96" selected={true}>{"96"}</option>
<option value="384">{"384"}</option>
<option value="1536">{"1536"}</option>
<option value="3456">{"3456"}</option>
</select>
<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" />
</form>
<form class="modal_close" method="dialog"><button /></form>
</dialog>
}
}
impl From<&PlateInstance> for String {
fn from(value: &PlateInstance) -> Self {
// Could have other formatting here
format!("{}, {}", value.name, value.plate.plate_format)
}
}

View File

@ -1,216 +0,0 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::Region;
// Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes;
const PALETTE: super::util::ColorPalette = Palettes::RAINBOW;
use super::super::transfer_menu::{num_to_letters, RegionDisplay};
#[derive(Properties, PartialEq)]
pub struct DestinationPlateProps {
pub source_plate: PlateInstance,
pub destination_plate: PlateInstance,
pub cell_height: f64,
}
#[function_component]
pub fn DestinationPlate(props: &DestinationPlateProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let m_start_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_end_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_stat_handle: UseStateHandle<bool> = use_state_eq(|| false);
if !(*m_stat_handle) {
let (pt1, pt2) = match ct_state.transfer.transfer_region.dest_region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0,0), (0,0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));
}
let destination_wells = ct_state.transfer.transfer_region.get_destination_wells();
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter()
.map(|x| x.id)
.collect();
ids.sort_unstable();
ids
};
let mouse_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
let m_stat_handle = m_stat_handle.clone();
Callback::from(move |(i, j, t)| match t {
MouseEventType::Mousedown => {
m_start_handle.set(Some((i, j)));
m_end_handle.set(Some((i, j)));
m_stat_handle.set(true);
}
MouseEventType::Mouseenter => {
if *m_stat_handle {
m_end_handle.set(Some((i, j)));
}
}
})
};
let transfer_map = {
let ts = main_state
.transfers
.iter()
.filter(|t| t.dest_id == props.destination_plate.get_uuid());
let mut tooltip_map: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new();
for t in ts {
let dws = t.transfer_region.get_destination_wells();
for dw in dws {
if let Some(val) = tooltip_map.get_mut(&dw) {
val.push(t);
} else {
tooltip_map.insert(dw, vec![t]);
}
}
}
tooltip_map
};
let mouseup_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
Callback::from(move |_: MouseEvent| {
m_stat_handle.set(false);
if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.0, ul.1, br.0, br.1)) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_region = Region::from(&rd);
});
}
}
}
})
};
let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_dest()");
});
let column_header = {
let headers = (1..=props.destination_plate.plate.size().1)
.map(|j| {
html! {<th>{format!("{:0>2}", j)}</th>}
})
.collect::<Html>();
html! {<tr><th />{ headers }</tr>}
};
let rows = (1..=props.destination_plate.plate.size().0)
.map(|i| {
let row_header = html! {<th>{num_to_letters(i)}</th>};
let row = (1..=props.destination_plate.plate.size().1).map(|j| {
html! {
<DestPlateCell i={i} j={j}
selected={super::source_plate::in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={destination_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={transfer_map.get(&(i,j))
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
}
cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", ")))}
/>
}
}).collect::<Html>();
html! {
<tr>
{ row_header }{ row }
</tr>
}
})
.collect::<Html>();
html! {
<div ondblclick={screenshot_callback}
class={classes!{"dest_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
}}
onmouseleave={move |e| {
mouseleave_callback.emit(e);
}}>
{ column_header }{ rows }
</table>
</div>
}
}
#[derive(Debug)]
pub enum MouseEventType {
Mousedown,
Mouseenter,
}
#[derive(Properties, PartialEq)]
pub struct DestPlateCellProps {
pub i: u8,
pub j: u8,
pub selected: bool,
pub mouse: Callback<(u8, u8, MouseEventType)>,
pub in_transfer: Option<bool>,
color: Option<[f64; 3]>,
cell_height: f64,
title: Option<String>,
}
#[function_component]
fn DestPlateCell(props: &DestPlateCellProps) -> Html {
let selected_class = match props.selected {
true => Some("current_select"),
false => None,
};
let in_transfer_class = match props.in_transfer {
Some(true) => Some("in_transfer"),
_ => None,
};
let color = props.color.unwrap_or([255.0, 255.0, 255.0]);
let mouse = Callback::clone(&props.mouse);
let mouse2 = Callback::clone(&props.mouse);
let (i, j) = (props.i, props.j);
html! {
<td class={classes!("plate_cell", selected_class, in_transfer_class)}
style={format!("height: {}px;", props.cell_height)}
onmousedown={move |_| {
mouse.emit((i,j, MouseEventType::Mousedown))
}}
onmouseenter={move |_| {
mouse2.emit((i,j, MouseEventType::Mouseenter))
}}>
<div class="plate_cell_inner"
style={format!("background: rgba({},{},{},1);", color[0], color[1], color[2])}
title={if let Some(text) = &props.title {
text.clone()
} else { "".to_string() }}/>
</td>
}
}

View File

@ -1,4 +0,0 @@
pub mod destination_plate;
pub mod plate_container;
pub mod source_plate;
mod util;

View File

@ -1,294 +0,0 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use yew::prelude::*;
use yewdux::prelude::*;
use crate::components::states::{CurrentTransfer, MainState};
use crate::data::plate_instances::PlateInstance;
use crate::data::transfer::Transfer;
use crate::data::transfer_region::Region;
// Color Palette for the Source Plates, can be changed here
use crate::components::plates::util::Palettes;
const PALETTE: super::util::ColorPalette = Palettes::RAINBOW;
use super::super::transfer_menu::{num_to_letters, RegionDisplay};
#[derive(PartialEq, Properties)]
pub struct SourcePlateProps {
pub source_plate: PlateInstance,
pub destination_plate: PlateInstance,
pub cell_height: f64,
}
#[function_component]
pub fn SourcePlate(props: &SourcePlateProps) -> Html {
let (main_state, _) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let m_start_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_end_handle: UseStateHandle<Option<(u8, u8)>> = use_state_eq(|| None);
let m_stat_handle: UseStateHandle<bool> = use_state_eq(|| false);
if !(*m_stat_handle) {
let (pt1, pt2) = match ct_state.transfer.transfer_region.source_region {
Region::Point((x, y)) => ((x, y), (x, y)),
Region::Rect(c1, c2) => (c1, c2),
Region::Custom(_) => ((0,0), (0,0)),
};
m_start_handle.set(Some(pt1));
m_end_handle.set(Some(pt2));
}
let transfer_map = {
let ts = main_state
.transfers
.iter()
.filter(|t| t.source_id == props.source_plate.get_uuid());
let mut tooltip_map: HashMap<(u8, u8), Vec<&Transfer>> = HashMap::new();
for t in ts {
let sws = t.transfer_region.get_source_wells();
for sw in sws {
if let Some(val) = tooltip_map.get_mut(&sw) {
val.push(t);
} else {
tooltip_map.insert(sw, vec![t]);
}
}
}
tooltip_map
};
let source_wells = ct_state.transfer.transfer_region.get_source_wells();
let ordered_ids: Vec<uuid::Uuid> = {
let mut ids: Vec<uuid::Uuid> = main_state.transfers.clone().iter()
.map(|x| x.id)
.collect();
ids.sort_unstable();
ids
};
let mouse_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
let m_stat_handle = m_stat_handle.clone();
Callback::from(move |(i, j, t)| match t {
MouseEventType::Mousedown => {
m_start_handle.set(Some((i, j)));
m_end_handle.set(Some((i, j)));
m_stat_handle.set(true);
}
MouseEventType::Mouseenter => {
if *m_stat_handle {
m_end_handle.set(Some((i, j)));
}
}
})
};
let mouseup_callback = {
let m_start_handle = m_start_handle.clone();
let m_end_handle = m_end_handle.clone();
Callback::from(move |_: MouseEvent| {
m_stat_handle.set(false);
if let Some(ul) = *m_start_handle {
if let Some(br) = *m_end_handle {
if let Ok(rd) = RegionDisplay::try_from((ul.0, ul.1, br.0, br.1)) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::from(&rd);
});
}
}
}
})
};
let mouseleave_callback = Callback::clone(&mouseup_callback);
let screenshot_callback = Callback::from(|_| {
let _ = js_sys::eval("copy_screenshot_src()");
});
let column_header = {
let headers = (1..=props.source_plate.plate.size().1)
.map(|j| {
html! {<th>
{format!("{:0>2}", j)}
</th>}
})
.collect::<Html>();
html! {<tr><th />{ headers }</tr>}
};
let rows = (1..=props.source_plate.plate.size().0)
.map(|i| {
let row_header = html! {<th>{num_to_letters(i)}</th>};
let row = (1..=props.source_plate.plate.size().1)
.map(|j| {
html! {
<SourcePlateCell i={i} j={j}
selected={in_rect(*m_start_handle.clone(), *m_end_handle.clone(), (i,j))}
mouse={mouse_callback.clone()}
in_transfer={source_wells.contains(&(i,j)) && main_state.preferences.in_transfer_hashes}
color={transfer_map.get(&(i,j))
.and_then(|t| t.last())
.map(|t| PALETTE.get_ordered(t.get_uuid(), &ordered_ids))
}
cell_height={props.cell_height}
title={transfer_map.get(&(i,j)).map(|transfers| format!("Used by: {}", transfers.iter().map(|t| t.name.clone())
.collect::<Vec<_>>().join(", ")))}
/>
}
})
.collect::<Html>();
html! {
<tr>
{ row_header }{ row }
</tr>
}
})
.collect::<Html>();
html! {
<div ondblclick={screenshot_callback}
class={classes!{"source_plate",
"W".to_owned()+&props.source_plate.plate.plate_format.to_string()}}>
<table
onmouseup={move |e| {
mouseup_callback.emit(e);
}}
onmouseleave={move |e| {
mouseleave_callback.emit(e);
}}>
{ column_header }
{ rows }
</table>
</div>
}
}
#[derive(PartialEq, Properties)]
pub struct SourcePlateCellProps {
i: u8,
j: u8,
selected: bool,
mouse: Callback<(u8, u8, MouseEventType)>,
in_transfer: Option<bool>,
color: Option<[f64; 3]>,
cell_height: f64,
title: Option<String>,
}
#[derive(Debug)]
pub enum MouseEventType {
Mousedown,
Mouseenter,
}
#[function_component]
fn SourcePlateCell(props: &SourcePlateCellProps) -> Html {
let selected_class = match props.selected {
true => Some("current_select"),
false => None,
};
let in_transfer_class = match props.in_transfer {
Some(true) => Some("in_transfer"),
_ => None,
};
let color = props.color.unwrap_or([255.0, 255.0, 255.0]);
let mouse = Callback::clone(&props.mouse);
let mouse2 = Callback::clone(&props.mouse);
let (i, j) = (props.i, props.j);
html! {
<td class={classes!("plate_cell", selected_class, in_transfer_class)}
style={format!("height: {}px;", props.cell_height)}
id={format!("color={:?}", props.color)}
onmousedown={move |_| {
mouse.emit((i,j, MouseEventType::Mousedown))
}}
onmouseenter={move |_| {
mouse2.emit((i,j, MouseEventType::Mouseenter))
}}>
<div class="plate_cell_inner"
style={format!("background: rgba({},{},{},1);", color[0], color[1], color[2])}
title={if let Some(text) = &props.title {
text.clone()
} else {"".to_string()}}/>
</td>
}
}
pub fn in_rect(corner1: Option<(u8, u8)>, corner2: Option<(u8, u8)>, pt: (u8, u8)) -> bool {
if let (Some(c1), Some(c2)) = (corner1, corner2) {
pt.0 <= u8::max(c1.0, c2.0)
&& pt.0 >= u8::min(c1.0, c2.0)
&& pt.1 <= u8::max(c1.1, c2.1)
&& pt.1 >= u8::min(c1.1, c2.1)
} else {
false
}
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::in_rect;
// in_rect tests
#[test]
#[wasm_bindgen_test]
fn test_in_rect1() {
// Test in center of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt = (5, 5);
assert!(in_rect(Some(c1), Some(c2), pt));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect2() {
// Test on top/bottom edges of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (1, 5);
let pt2 = (10, 5);
assert!(in_rect(Some(c1), Some(c2), pt1));
assert!(in_rect(Some(c1), Some(c2), pt2));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt1));
assert!(in_rect(Some(c2), Some(c1), pt2));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect3() {
// Test on left/right edges of rect
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (5, 1);
let pt2 = (5, 10);
assert!(in_rect(Some(c1), Some(c2), pt1));
assert!(in_rect(Some(c1), Some(c2), pt2));
// Order of the corners should not matter:
assert!(in_rect(Some(c2), Some(c1), pt1));
assert!(in_rect(Some(c2), Some(c1), pt2));
}
#[test]
#[wasm_bindgen_test]
fn test_in_rect4() {
// Test cases that should fail
let c1 = (1, 1);
let c2 = (10, 10);
let pt1 = (0, 0);
let pt2 = (15, 15);
assert!(!in_rect(Some(c1), Some(c2), pt1));
assert!(!in_rect(Some(c1), Some(c2), pt2));
}
}

View File

@ -1,512 +0,0 @@
#![allow(non_snake_case)]
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{EventTarget, HtmlInputElement};
use yew::prelude::*;
use yewdux::prelude::*;
use crate::data::{transfer::Transfer, transfer_region::Region};
use super::states::{CurrentTransfer, MainState};
#[function_component]
pub fn TransferMenu() -> Html {
let (main_state, main_dispatch) = use_store::<MainState>();
let (ct_state, ct_dispatch) = use_store::<CurrentTransfer>();
let on_name_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if input.value() == "" {
return;
} // We do not want empty inputs!
ct_dispatch.reduce_mut(|state| {
state.transfer.name = input.value();
});
}
})
};
let on_src_region_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
if matches!(ct_dispatch.get().transfer.transfer_region.source_region, Region::Custom(_)) {
return; // Do nothing here!
}
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(rd) = RegionDisplay::try_from(input.value().to_uppercase()) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.source_region = Region::from(&rd);
});
input.set_custom_validity("");
} else {
input.set_custom_validity("Invalid region.")
}
}
})
};
let on_dest_region_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(rd) = RegionDisplay::try_from(input.value().to_uppercase()) {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.dest_region = Region::from(&rd);
});
input.set_custom_validity("");
} else {
input.set_custom_validity("Invalid region.")
}
}
})
};
let on_source_interleave_x_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_source =
(num, state.transfer.transfer_region.interleave_source.1);
});
}
}
})
};
let on_source_interleave_y_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_source =
(state.transfer.transfer_region.interleave_source.0, num);
});
}
}
})
};
let on_dest_interleave_x_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_dest =
(num, state.transfer.transfer_region.interleave_dest.1);
});
}
}
})
};
let on_dest_interleave_y_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let target: Option<EventTarget> = e.target();
let input = target.and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
if let Some(input) = input {
if let Ok(num) = input.value().parse::<i8>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.transfer_region.interleave_dest =
(state.transfer.transfer_region.interleave_dest.0, num);
});
}
}
})
};
let on_volume_change = {
let ct_dispatch = ct_dispatch.clone();
Callback::from(move |e: Event| {
let input = e
.target()
.expect("Event must have target")
.dyn_into::<HtmlInputElement>()
.expect("Must have been emitted by input");
if let Ok(num) = input.value().parse::<f32>() {
ct_dispatch.reduce_mut(|state| {
state.transfer.volume = num;
});
}
})
};
let new_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
Callback::from(move |_: MouseEvent| {
main_dispatch.reduce_mut(|state| {
state.selected_transfer = Uuid::nil();
});
ct_dispatch.reduce_mut(|state| {
state.transfer = Transfer::default();
state.transfer.source_id = main_state.selected_source_plate;
state.transfer.dest_id = main_state.selected_dest_plate;
});
})
};
let save_transfer_button_callback = {
let main_dispatch = main_dispatch.clone();
let main_state = main_state.clone();
let ct_state = ct_state.clone();
let new_transfer_button_callback = new_transfer_button_callback.clone();
Callback::from(move |e: MouseEvent| {
log::debug!("Button pressed");
if main_state.selected_transfer.is_nil() {
if let Some(spi) = main_state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == main_state.selected_source_plate)
{
if let Some(dpi) = main_state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == main_state.selected_dest_plate)
{
let new_transfer = Transfer::new(
spi.clone(),
dpi.clone(),
ct_state.transfer.transfer_region.clone(),
ct_state.transfer.name.clone(),
);
main_dispatch.reduce_mut(|state| {
state.transfers.push(new_transfer);
state.selected_transfer = state
.transfers
.last()
.expect("An element should have just been added")
.get_uuid();
});
new_transfer_button_callback.emit(e); // If we just made a new transfer,
// then we should make another on
// save.
}
}
} else if let Some(index) = main_state
.transfers
.iter()
.position(|t| t.get_uuid() == main_state.selected_transfer)
{
main_dispatch.reduce_mut(|state| {
state.transfers[index] = ct_state.transfer.clone();
});
}
})
};
let delete_transfer_button_callback = {
let ct_state = ct_state.clone();
let new_callback = new_transfer_button_callback.clone();
Callback::from(move |e: MouseEvent| {
if main_state.selected_transfer.is_nil() {
// Maybe reset transfer?
} else if let Some(index) = main_state
.transfers
.iter()
.position(|t| t.get_uuid() == ct_state.transfer.get_uuid())
{
main_dispatch.reduce_mut(|state| {
state.transfers.remove(index);
state.selected_transfer = Uuid::nil();
});
new_callback.emit(e); // We need a new transfer now
}
})
};
html! {
<div class="transfer_menu">
<form>
<div>
<label for="name"><h3>{"Name:"}</h3></label>
<input type="text" name="name"
onchange={on_name_change}
value={ct_state.transfer.name.clone()}/>
</div>
// Anything below here is not rendered when a Custom transfer is selected
if !matches!(&ct_state.transfer.transfer_region.source_region, Region::Custom(_)) {
<div>
<label for="src_region"><h3>{"Source Region:"}</h3></label>
<input type="text" name="src_region"
onchange={on_src_region_change}
value={RegionDisplay::from(&ct_state.transfer.transfer_region.source_region).text}/>
</div>
<div>
<label for="dest_region"><h3>{"Destination Region:"}</h3></label>
<input type="text" name="dest_region"
onchange={on_dest_region_change}
value={RegionDisplay::from(&ct_state.transfer.transfer_region.dest_region).text}/>
</div>
<div>
<h3>{"Source Interleave "}</h3>
<label for="source_interleave_x">{"Row:"}</label>
<input type="number" name="source_interleave_x"
onchange={on_source_interleave_x_change}
value={ct_state.transfer.transfer_region.interleave_source.0.to_string()}/>
<label for="source_interleave_y">{"Col:"}</label>
<input type="number" name="source_interleave_y"
onchange={on_source_interleave_y_change}
value={ct_state.transfer.transfer_region.interleave_source.1.to_string()}/>
</div>
<div>
<h3>{"Destination Interleave "}</h3>
<label for="dest_interleave_x">{"Row:"}</label>
<input type="number" name="dest_interleave_x"
onchange={on_dest_interleave_x_change}
value={ct_state.transfer.transfer_region.interleave_dest.0.to_string()}/>
<label for="dest_interleave_y">{"Col:"}</label>
<input type="number" name="dest_interleave_y"
onchange={on_dest_interleave_y_change}
value={ct_state.transfer.transfer_region.interleave_dest.1.to_string()}/>
</div>
<div>
<label for="volume"><h3>{"Volume"}</h3></label>
<input type="number" name="volume" class="volume_input"
min="0" step="0.1"
onchange={on_volume_change}
value={ct_state.transfer.volume.to_string()}/>
</div>
}
<div id="controls">
<input type="button" name="new_transfer" onclick={new_transfer_button_callback}
value={"New"} />
<input type="button" name="save_transfer" onclick={save_transfer_button_callback}
value={"Save"} />
<input type="button" name="delete_transfer" onclick={delete_transfer_button_callback}
value={"Delete"} />
</div>
</form>
</div>
}
}
#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
pub struct RegionDisplay {
pub text: String,
pub col_start: u8,
pub row_start: u8,
pub col_end: u8,
pub row_end: u8,
}
impl TryFrom<String> for RegionDisplay {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(&value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?;
let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?;
let row_start: u8 = captures[2]
.parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
Ok(RegionDisplay {
text: value,
col_start,
row_start,
col_end,
row_end,
})
} else {
Err("Regex match failed")
}
}
}
impl TryFrom<&str> for RegionDisplay {
type Error = &'static str;
fn try_from(value: &str) -> Result<Self, Self::Error> {
lazy_static! {
static ref REGION_REGEX: Regex = Regex::new(r"([A-Z]+)(\d+):([A-Z]+)(\d+)").unwrap();
}
if let Some(captures) = REGION_REGEX.captures(&value) {
if captures.len() != 5 {
return Err("Not enough capture groups");
}
let col_start = letters_to_num(&captures[1]).ok_or("Column start failed to parse")?;
let col_end = letters_to_num(&captures[3]).ok_or("Column end failed to parse")?;
let row_start: u8 = captures[2]
.parse::<u8>()
.or(Err("Row start failed to parse"))?;
let row_end: u8 = captures[4]
.parse::<u8>()
.or(Err("Row end failed to parse"))?;
Ok(RegionDisplay {
text: value.to_string(),
col_start,
row_start,
col_end,
row_end,
})
} else {
Err("Regex match failed")
}
}
}
impl From<&Region> for RegionDisplay {
fn from(value: &Region) -> Self {
match *value {
Region::Point((col, row)) => {
RegionDisplay::try_from((col, row, col, row)).ok().unwrap()
}
Region::Rect(c1, c2) => RegionDisplay::try_from((c1.0, c1.1, c2.0, c2.1))
.ok()
.unwrap(),
Region::Custom(_) => RegionDisplay { text: "CUSTOM".to_string(), col_start: 0, row_start: 0, col_end: 0, row_end: 0 }
}
}
}
impl From<&RegionDisplay> for Region {
fn from(value: &RegionDisplay) -> Self {
if value.col_start == value.col_end && value.row_start == value.row_end {
Region::Point((value.col_start, value.row_start))
} else {
Region::Rect(
(value.col_start, value.row_start),
(value.col_end, value.row_end),
)
}
}
}
impl TryFrom<(u8, u8, u8, u8)> for RegionDisplay {
type Error = &'static str;
fn try_from(value: (u8, u8, u8, u8)) -> Result<Self, Self::Error> {
// (Column Start, Row Start, Column End, Row End)
// This can only possibly fail if one of the coordinates is zero...
let cs = num_to_letters(value.0).ok_or("Column start failed to parse")?;
let ce = num_to_letters(value.2).ok_or("Column end failed to parse")?;
Ok(RegionDisplay {
text: format!("{}{}:{}{}", cs, value.1, ce, value.3),
col_start: value.0,
row_start: value.1,
col_end: value.2,
row_end: value.3,
})
}
}
pub fn letters_to_num(letters: &str) -> Option<u8> {
let mut num: u8 = 0;
for (i, letter) in letters.to_ascii_uppercase().chars().rev().enumerate() {
log::debug!("{}, {}", i, letter);
let n = letter as u8;
if !(65..=90).contains(&n) {
return None;
}
num = num.checked_add((26_i32.pow(i as u32) * (n as i32 - 64)).try_into().ok()?)?;
}
Some(num)
}
pub fn num_to_letters(num: u8) -> Option<String> {
if num == 0 {
return None;
} // Otherwise, we will not return none!
// As another note, we can't represent higher than "IV" anyway;
// thus there's no reason for a loop (26^n with n>1 will NOT occur).
let mut text = "".to_string();
let mut digit1 = num.div_euclid(26u8);
let mut digit2 = num.rem_euclid(26u8);
if digit1 > 0 && digit2 == 0u8 {
digit1 -= 1;
digit2 = 26;
}
if digit1 != 0 {
text.push((64 + digit1) as char)
}
text.push((64 + digit2) as char);
Some(text.to_string())
}
#[cfg(test)]
mod tests {
use wasm_bindgen_test::*;
use super::{letters_to_num, num_to_letters, RegionDisplay};
#[test]
#[wasm_bindgen_test]
fn test_letters_to_num() {
assert_eq!(letters_to_num("D"), Some(4));
assert_eq!(letters_to_num("d"), None);
assert_eq!(letters_to_num("AD"), Some(26 + 4));
assert_eq!(letters_to_num("CG"), Some(3 * 26 + 7));
}
#[test]
#[wasm_bindgen_test]
fn test_num_to_letters() {
println!("27 is {:?}", num_to_letters(27));
assert_eq!(num_to_letters(1), Some("A".to_string()));
assert_eq!(num_to_letters(26), Some("Z".to_string()));
assert_eq!(num_to_letters(27), Some("AA".to_string()));
assert_eq!(num_to_letters(111), Some("DG".to_string()));
}
#[test]
#[wasm_bindgen_test]
fn test_l2n_and_n2l() {
assert_eq!(
num_to_letters(letters_to_num("A").unwrap()),
Some("A".to_string())
);
assert_eq!(
num_to_letters(letters_to_num("BJ").unwrap()),
Some("BJ".to_string())
);
for i in 1..=255 {
assert_eq!(letters_to_num(&num_to_letters(i).unwrap()), Some(i));
}
}
#[test]
#[wasm_bindgen_test]
fn test_try_from_string_for_regiondisplay() {
let desired = RegionDisplay {
text: "A1:E5".to_string(),
row_start: 1,
row_end: 5,
col_start: 1,
col_end: 5,
};
assert_eq!(desired, "A1:E5".to_string().try_into().unwrap());
}
}

View File

@ -1,81 +0,0 @@
use crate::components::states::MainState;
use crate::components::transfer_menu::num_to_letters;
use crate::data::transfer::Transfer;
use serde::{Serialize, Deserialize};
use std::error::Error;
#[derive(Serialize, Deserialize, Debug)]
pub struct TransferRecord {
#[serde(rename = "Source Plate")]
pub source_plate: String,
#[serde(rename = "Source Well")]
pub source_well: String,
#[serde(rename = "Dest Plate")]
pub destination_plate: String,
#[serde(rename = "Destination Well")]
pub destination_well: String,
#[serde(rename = "Transfer Volume")]
pub volume: f32,
#[serde(rename = "Concentration")]
pub concentration: Option<f32>,
}
pub fn state_to_csv(state: &MainState) -> Result<String, Box<dyn Error>> {
let mut records: Vec<TransferRecord> = Vec::new();
for transfer in &state.transfers {
let src_barcode = state
.source_plates
.iter()
.find(|spi| spi.get_uuid() == transfer.source_id)
.ok_or("Found unpurged transfer")?;
let dest_barcode = state
.destination_plates
.iter()
.find(|dpi| dpi.get_uuid() == transfer.dest_id)
.ok_or("Found unpurged transfer")?;
records.append(&mut transfer_to_records(
transfer,
&src_barcode.name,
&dest_barcode.name,
))
}
return records_to_csv(records);
}
fn transfer_to_records(
tr: &Transfer,
src_barcode: &str,
dest_barcode: &str,
) -> Vec<TransferRecord> {
let source_wells = tr.transfer_region.get_source_wells();
let map = tr.transfer_region.calculate_map();
let mut records: Vec<TransferRecord> = vec![];
for s_well in source_wells {
let dest_wells = map(s_well);
if let Some(dest_wells) = dest_wells {
for d_well in dest_wells {
records.push(TransferRecord {
source_plate: src_barcode.to_string(),
source_well: format!("{}{}", num_to_letters(s_well.0).unwrap(), s_well.1),
destination_plate: dest_barcode.to_string(),
destination_well: format!("{}{}", num_to_letters(d_well.0).unwrap(), d_well.1),
volume: tr.volume,
concentration: None,
})
}
}
}
records
}
fn records_to_csv(trs: Vec<TransferRecord>) -> Result<String, Box<dyn Error>> {
let mut wtr = csv::WriterBuilder::new().from_writer(vec![]);
for record in trs {
wtr.serialize(record)?
}
let data = String::from_utf8(wtr.into_inner()?)?;
Ok(data)
}

View File

@ -1,37 +0,0 @@
#![allow(non_snake_case)]
mod components;
mod data;
use components::main_window::MainWindow;
use yew::prelude::*;
#[cfg(debug_assertions)]
use data::*;
#[function_component]
pub fn App() -> Html {
html! {
<MainWindow />
}
}
#[cfg(debug_assertions)]
pub fn plate_test() {
let source = plate::Plate::new(plate::PlateType::Source, plate::PlateFormat::W96);
let destination = plate::Plate::new(plate::PlateType::Destination, plate::PlateFormat::W384);
let transfer = transfer_region::TransferRegion {
source_plate: source,
source_region: transfer_region::Region::Rect((1, 1), (2, 2)),
dest_plate: destination,
dest_region: transfer_region::Region::Rect((2, 2), (11, 11)),
interleave_source: (1, 1),
interleave_dest: (3, 3),
};
println!("{}", transfer);
let sws = transfer.get_source_wells();
let m = transfer.calculate_map();
for w in sws {
println!("{:?} -> {:?}", w, m(w));
}
}