├── .gitignore
├── .idea
├── .gitignore
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
├── plasmid_check.iml
└── vcs.xml
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── build.rs
├── file_spec.md
├── install_scripts
├── plascad.desktop
└── setup_linux_desktop.sh
├── rustfmt.toml
├── screenshots
├── map_aug_24.png
├── primers_aug_24.png
├── protein_aug_24.png
└── seq_aug_24.png
└── src
├── ab1.rs
├── alignment.rs
├── alignment_map.rs
├── backbones.rs
├── cloning.rs
├── external_websites.rs
├── feature_db_builder.rs
├── feature_db_load.rs
├── file_io
├── ab1.rs
├── ab1_tags.rs
├── genbank.rs
├── mod.rs
├── pcad.rs
├── save.rs
└── snapgene.rs
├── gui
├── ab1.rs
├── alignment.rs
├── circle.rs
├── cloning.rs
├── feature_table.rs
├── input.rs
├── ligation.rs
├── lin_maps.rs
├── metadata.rs
├── mod.rs
├── navigation.rs
├── pcr.rs
├── portions.rs
├── primer_table.rs
├── protein.rs
├── save.rs
├── sequence
│ ├── feature_overlay.rs
│ ├── mod.rs
│ ├── primer_overlay.rs
│ └── seq_view.rs
└── theme.rs
├── ligation.rs
├── main.rs
├── melting_temp_calcs.rs
├── misc_types.rs
├── pcr.rs
├── portions.rs
├── primer.rs
├── primer_metrics.rs
├── protein.rs
├── reading_frame.rs
├── resources
├── icon.ico
└── icon.png
├── save_compat.rs
├── solution_helper.rs
├── state.rs
├── tags.rs
├── toxic_proteins.rs
└── util.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | # Rust
2 | /target
3 | Cargo.lock
4 |
5 | # IDE
6 | *.iws
7 | workspace.xml
8 |
9 |
10 | # Saved file formats
11 | *.save
12 | *.pcad
13 | *.fasta
14 | *.fa
15 | *.dna
16 | *.xml
17 | *.pp
18 |
19 | # I'm not sure what is generating these.
20 | *.rs~
21 | *.md~
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/plasmid_check.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "plascad"
3 | version = "0.7.8"
4 | edition = "2024"
5 | authors = ["David O'Connor "]
6 | #description = "Tools for plasmid and primer design, PCR, and related."
7 | description = "PlasCAD"
8 | keywords = ["plasmid", "vector", "primer", "biology", "pcr"]
9 | categories = [
10 | "science", "science::bioinformatics",
11 | ]
12 | repository = "https://github.com/David-OConnor/plascad"
13 | documentation = "https://docs.rs/plascad"
14 | readme = "README.md"
15 | license = "MIT"
16 | exclude = [".gitignore"]
17 | default-run = "plascad"
18 |
19 |
20 | [[bin]]
21 | name = "plascad"
22 | path = "src/main.rs"
23 |
24 | #[[bin]]
25 | #name = "feature_db_builder"
26 | #path = "src/feature_db_builder.rs"
27 |
28 | [dependencies]
29 | eframe = "0.31.0"
30 | egui_extras = "0.31.0" # For tables.
31 | egui-file-dialog = "0.10.0" # For file dialogs.
32 |
33 | chrono = "0.4.41"
34 |
35 | serde = { version = "1.0.216", features = ["derive"] }
36 | num_enum = "0.7.3" # reversing a u8-repr.
37 |
38 | # For FASTA read and write. Note: We can ditch this for a custom parser; FASTA is easy.
39 | # Note, 2024-08-09: Removing this appears to have minimal impact on binary size.
40 | bio = "2.2.0"
41 | quick-xml = {version = "0.37.1", features = ["serialize"]} # For Snapgene feature parsing and writing.
42 | # For parsing and writing GenBank files. todo: This seems to have a lot of sub-deps; Increases binary size a bit.
43 | gb-io = "0.9.0"
44 |
45 | lin_alg = "1.1.8"
46 | na_seq = "0.2.5"
47 | bio_apis = "0.1.0"
48 |
49 | # We use bincode for saving and loading to files using our custom format.
50 | bincode = "2.0.1"
51 |
52 | # For creating JSON payloads for the PDB search API.
53 | serde_json = {version = "1.0.127"}
54 |
55 | # For clipboard operations not supported directly by EGUI
56 | copypasta = "0.10.1"
57 | #winreg = "0.52.0" # For setting up file associations on Windows
58 |
59 | webbrowser = "1.0.1"
60 | # We use strum to iterate over enums.
61 | # todo: Check sup-deps/binary impact etc
62 | strum = "0.27.1"
63 | strum_macros = "0.27.1"
64 |
65 | # We use Flate2 for decompressing BAM files. todo: Remove if you remove that functionality.
66 | flate2 = "1.1.1"
67 | bgzip = "0.3.1"
68 |
69 |
70 | [patch.crates-io]
71 | na_seq = { path = "../na_seq" }
72 | lin_alg = { path = "../../lin_alg" }
73 | bio_apis = { path = "../bio_apis" }
74 |
75 |
76 |
77 | [build-dependencies]
78 | # These are for embedding an application icon, on Windows.
79 | winresource = "0.1.20"
80 |
81 | # https://doc.rust-lang.org/cargo/reference/profiles.html
82 | [profile.release]
83 | strip = true # Strip symbols from binary. Very little improvement in size.
84 | # Optimize for size. May make it tough to use with a debugger. Possibly could make it slower(?); the docs imply
85 | # experimenting is required.
86 | # opt-level = 'z' # Optimize for binary size; gives about 60% of the original size.
87 | # opt-level = 's' # Optimize for binary size; gives about 60% of the original size.
88 | #codegen-units = 1 # Small decrease in binary size; longer compile time.
89 | lto = true # Can produce better optimized code, at the cost of [significantly]longer linking time.
90 | # Very little size improvement.
91 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2025 David O'Connor
2 |
3 | Permission is hereby granted, free of charge, to any
4 | person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the
6 | Software without restriction, including without
7 | limitation the rights to use, copy, modify, merge,
8 | publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software
10 | is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice
14 | shall be included in all copies or substantial portions
15 | of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 | DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | // For embedding application icons, in Windows.
2 |
3 | use std::{env, io};
4 |
5 | use winresource::WindowsResource;
6 |
7 | fn main() -> io::Result<()> {
8 | if env::var_os("CARGO_CFG_WINDOWS").is_some() {
9 | WindowsResource::new()
10 | // This path can be absolute, or relative to your crate root.
11 | .set_icon("src/resources/icon.ico")
12 | .compile()?;
13 | }
14 | Ok(())
15 | }
16 |
--------------------------------------------------------------------------------
/file_spec.md:
--------------------------------------------------------------------------------
1 | # The PlasCAD file format
2 |
3 | This document describes the PlasCAD file format: a compact way of storing DNA sequences, features, primers, metadata,
4 | and related information. It's a binary format divided into discrete packets, and uses the `.pcad` file extension.
5 | Code for implementing can be found in [pcad.rs](src/file_io/pcad.rs).
6 |
7 | Most data structures use [Bincode](https://docs.rs/bincode/latest/bincode/) library. This is convenient for this program's
8 | purposes, but makes external interoperability more challenging.
9 |
10 | Byte order is big endian.
11 |
12 |
13 | ## Components
14 | The starting two bytes of a PlasCAD file are always `0xca`, `0xfe`.
15 |
16 | The remaining bytes are divided into adjacent packets. Packets can be found in any order.
17 |
18 |
19 | ## Packet structure
20 | - **Byte 0**: Always `0x11`
21 | - **Bytes 1-4**: A 32-bit unsigned integer of payload size, in bytes.
22 | - **Byte 5**: An 8-bit unsigned integer that indicates the packet's type. (See the `Packets` sections below for this mapping.)
23 | - **Bytes 6-end**: The payload; how this is encoded depends on packet type.
24 |
25 | ## Packet types
26 |
27 | ### Sequence: 0
28 | Contains a DNA sequence.
29 |
30 | Bytes 0-3: A 32-bit unsigned integer of sequence length, in nucleotides.
31 | Remaining data: Every two bits is a nucleotide. This packet is always a whole number of bytes; bits in the final byte that
32 | would extend past the sequence length are ignored. Bit assignments for nucleotides is as follows:
33 |
34 | - **T**: `0b00`
35 | - **C**: `0b01`
36 | - **A**: `0b10`
37 | - **G**: `0b11`
38 |
39 | This is the same nucleotide mapping as [.2bit format](http://genome.ucsc.edu/FAQ/FAQformat.html#format7).
40 |
41 | #### An example
42 | The sequence `CTGATTTCTG`. This would serialize as follows, using 7 total bytes:
43 |
44 | - **Bytes 0-3**: `[0, 0, 0, 10]`, to indicate the sequence length of 10 nucleodies.
45 |
46 | Three additional bytes to encode the sequence; each byte can fit 4 nucleotides:
47 | - **Byte 4**: `CTGA` `0b01_00_11_10`
48 | - **Byte 5**: `TTTC` `0b00_00_00_01`
49 | - **Byte 6**: `TG` `0b00_11_00_00`.
50 |
51 | On the final byte, note the 0-fill on the right; we know not to encode it as `T` due to the
52 | sequence length.
53 |
54 |
55 | ## Packets, with their associcated integer type
56 |
57 | ### Features: 1
58 | A bincode serialization of a `Vec`
59 |
60 | ### Primers: 2
61 | A bincode serialization of a `Vec`
62 |
63 | ### Metadata: 3
64 | A bincode serialization of a `Metadata`
65 |
66 | ### IonConcentrations: 6
67 | A bincode serialization of a `IonConcentrations`
68 |
69 | ### Portions: 7
70 | A bincode serialization of a `Portions`
71 |
72 | ### PathLoaded: 10
73 | A bincode serialization of a `Option`
74 |
75 | ### Topology: 11
76 | A bincode serialization of a `Topology`
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/install_scripts/plascad.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=PlasCAD
3 | Exec=~/plascad/plascad
4 | Icon=~/plascad/icon.png
5 | Type=Application
6 | Terminal=false
7 | Categories=Development;Science;Biology;
8 | Comment=Plasmid and Primer editing tools
--------------------------------------------------------------------------------
/install_scripts/setup_linux_desktop.sh:
--------------------------------------------------------------------------------
1 | # This file sets up a Linux desktop entry, and moves the application to the home directory.
2 |
3 | NAME_UPPER="PlasCAD"
4 | NAME="plascad"
5 |
6 | printf "Moving the ${NAME_UPPER} executable and icon to ~/${NAME}..."
7 |
8 | chmod +x $NAME
9 |
10 | if [ ! -d ~/${NAME} ]; then
11 | mkdir ~/${NAME}
12 | fi
13 |
14 | cp ${NAME} ~/${NAME}
15 | cp icon.png ~/${NAME}/icon.png
16 |
17 | # Update the desktop entry with the absolute path.
18 | sed "s|~|$HOME|g" ${NAME}.desktop > ~/.local/share/applications/${NAME}.desktop
19 |
20 | printf "\nComplete. You can launch ${NAME_UPPER} through the GUI, eg search \"${NAME_UPPER}\", and/or add to favorites.\n"
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | imports_granularity = "Crate"
2 | group_imports = "StdExternalCrate"
--------------------------------------------------------------------------------
/screenshots/map_aug_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/screenshots/map_aug_24.png
--------------------------------------------------------------------------------
/screenshots/primers_aug_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/screenshots/primers_aug_24.png
--------------------------------------------------------------------------------
/screenshots/protein_aug_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/screenshots/protein_aug_24.png
--------------------------------------------------------------------------------
/screenshots/seq_aug_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/screenshots/seq_aug_24.png
--------------------------------------------------------------------------------
/src/ab1.rs:
--------------------------------------------------------------------------------
1 | //! For operations pertaining to AB1 (Applied Biosystem's sequencing) trace sequence data.
2 |
3 | use std::collections::HashMap;
4 |
5 | use bincode::{Decode, Encode};
6 | use na_seq::Seq;
7 |
8 | /// The data structure representing AB1 data.
9 | #[derive(Clone, Debug, Default, Encode, Decode)]
10 | pub struct SeqRecordAb1 {
11 | pub id: String,
12 | pub name: String,
13 | pub description: String,
14 | pub sequence: Seq,
15 | pub sequence_user: Option,
16 | pub annotations: HashMap,
17 | pub quality: Option>,
18 | pub quality_user: Option>,
19 | pub peak_heights: Vec,
20 | /// Analyzed data, for each channel.
21 | /// G
22 | pub data_ch1: Vec,
23 | /// A
24 | pub data_ch2: Vec,
25 | /// T
26 | pub data_ch3: Vec,
27 | /// C
28 | pub data_ch4: Vec,
29 | /// Peak locations.
30 | pub peak_locations: Vec,
31 | /// Peak locations edited by user.
32 | pub peak_locations_user: Option>,
33 | }
34 |
--------------------------------------------------------------------------------
/src/alignment.rs:
--------------------------------------------------------------------------------
1 | use bio::alignment::{
2 | Alignment,
3 | distance::simd::{bounded_levenshtein, hamming, levenshtein},
4 | pairwise::Aligner,
5 | };
6 | use na_seq::{AminoAcid, Nucleotide, Seq, seq_aa_to_u8_lower, seq_to_u8_lower};
7 |
8 | #[derive(Clone, Copy, PartialEq)]
9 | pub enum AlignmentMode {
10 | Dna,
11 | AminoAcid,
12 | }
13 |
14 | impl Default for AlignmentMode {
15 | fn default() -> Self {
16 | Self::Dna
17 | }
18 | }
19 |
20 | #[derive(Default)]
21 | pub struct AlignmentState {
22 | pub seq_a: Seq,
23 | pub seq_b: Seq,
24 | pub seq_aa_a: Vec,
25 | pub seq_aa_b: Vec,
26 | // todo: Perhaps these inputs are better fit for State_ui.
27 | pub seq_a_input: String,
28 | pub seq_b_input: String,
29 | pub alignment_result: Option,
30 | pub dist_result: Option,
31 | pub text_display: String, // Ie `AlignmentResult::pretty`.
32 | pub mode: AlignmentMode,
33 | }
34 |
35 | #[derive(Clone, Copy)]
36 | /// Use Hamming Distance if:
37 | /// Sequences are of the same length.
38 | /// You're only looking for substitutions and the sequences are already aligned.
39 | /// Fast, low-complexity calculations are a priority.
40 | /// Use Levenshtein Distance if:
41 | /// Sequences vary in length.
42 | /// Insertions, deletions, and substitutions are all meaningful for your comparison.
43 | /// You need a precise measure of distance for downstream processing.
44 | /// Use Bounded Levenshtein Distance if:
45 | /// You have a known tolerance or threshold for "closeness."
46 | /// You want to filter out sequences that are too different without computing the full distance.
47 | /// Speed and scalability are critical considerations.
48 | pub enum DistanceType {
49 | Hamming,
50 | Levenshtein,
51 | /// Inner: u32
52 | BoundedLevenshtein(u32),
53 | }
54 |
55 | // todo: Which dist algo? bounded_levenshtein, hamming, levenshtein?
56 | /// Accepts UTF-8 byte representation of letters.
57 | fn distance(alpha: &[u8], beta: &[u8]) -> u64 {
58 | let dist_type = if alpha.len() == beta.len() {
59 | DistanceType::Hamming
60 | } else {
61 | DistanceType::Levenshtein
62 | };
63 |
64 | match dist_type {
65 | DistanceType::Hamming => hamming(&alpha, &beta),
66 | DistanceType::Levenshtein => levenshtein(&alpha, &beta) as u64,
67 | DistanceType::BoundedLevenshtein(k) => {
68 | bounded_levenshtein(&alpha, &beta, k).unwrap() as u64
69 | }
70 | }
71 | }
72 |
73 | pub fn distance_nt(alpha: &[Nucleotide], beta: &[Nucleotide]) -> u64 {
74 | let alpha_ = seq_to_u8_lower(alpha);
75 | let beta_ = seq_to_u8_lower(beta);
76 | distance(&alpha_, &beta_)
77 | }
78 |
79 | pub fn distance_aa(alpha: &[AminoAcid], beta: &[AminoAcid]) -> u64 {
80 | let alpha_ = seq_aa_to_u8_lower(alpha);
81 | let beta_ = seq_aa_to_u8_lower(beta);
82 | distance(&alpha_, &beta_)
83 | }
84 |
85 | fn align_pairwise(seq_0: &[u8], seq_1: &[u8]) -> (Alignment, String) {
86 | // todo: Lots of room to configure this.
87 | let score = |a: u8, b: u8| if a == b { 1i32 } else { -1i32 };
88 |
89 | let mut aligner = Aligner::with_capacity(seq_0.len(), seq_1.len(), -5, -1, &score);
90 |
91 | // todo: Global? Semiglobal? Local?
92 | let result = aligner.semiglobal(seq_0, seq_1);
93 |
94 | let text = result.pretty(seq_0, seq_1, 120);
95 |
96 | (result, text)
97 | }
98 |
99 | pub fn align_pairwise_nt(seq_0: &[Nucleotide], seq_1: &[Nucleotide]) -> (Alignment, String) {
100 | // todo: Lots of room to configure this.
101 | let seq_0_ = seq_to_u8_lower(seq_0);
102 | let seq_1_ = seq_to_u8_lower(seq_1);
103 |
104 | align_pairwise(&seq_0_, &seq_1_)
105 | }
106 |
107 | pub fn align_pairwise_aa(seq_0: &[AminoAcid], seq_1: &[AminoAcid]) -> (Alignment, String) {
108 | // todo: Lots of room to configure this.
109 | let seq_0_ = seq_aa_to_u8_lower(seq_0);
110 | let seq_1_ = seq_aa_to_u8_lower(seq_1);
111 |
112 | align_pairwise(&seq_0_, &seq_1_)
113 | }
114 |
--------------------------------------------------------------------------------
/src/alignment_map.rs:
--------------------------------------------------------------------------------
1 | //! Data structures and related code for BAM and SAM alignment map data.
2 | //! [SAM/BAM spec document, CAO Nov 2024](https://samtools.github.io/hts-specs/SAMv1.pdf)
3 | //!
4 | //! BAM format is little endian.
5 |
6 | use std::{
7 | fs::File,
8 | io,
9 | io::{BufRead, ErrorKind, Read},
10 | path::Path,
11 | };
12 |
13 | use bgzip::{BGZFError, BGZFReader, index::BGZFIndex, read::IndexedBGZFReader};
14 | use flate2::read::{GzDecoder, MultiGzDecoder};
15 |
16 | /// Syntax helper for parsing multi-byte fields into primitives.
17 | ///
18 | /// Example: `parse_le!(bytes, i32, 5..9);`
19 | #[macro_export]
20 | macro_rules! parse_le {
21 | ($bytes:expr, $t:ty, $range:expr) => {{ <$t>::from_le_bytes($bytes[$range].try_into().unwrap()) }};
22 | }
23 |
24 | const MAGIC: [u8; 4] = [b'B', b'A', b'M', 1];
25 |
26 | #[derive(Debug)]
27 | struct RefSeq {
28 | pub name: String,
29 | pub l_ref: u32,
30 | }
31 |
32 | impl RefSeq {
33 | /// Deserialize from BAM.
34 | pub fn from_buf(buf: &[u8]) -> io::Result {
35 | let l_name = parse_le!(buf, u32, 0..4);
36 | let name_end = 4 + l_name as usize;
37 |
38 | Ok(Self {
39 | // todo: Don't unwrap this string parsing; map the error
40 | // name_end - 1: Don't parse the trailing NUL char. (In the spec, this is a null-terminated string)
41 | name: String::from_utf8(buf[4..name_end - 1].to_vec()).unwrap(),
42 | l_ref: parse_le!(buf, u32, name_end..name_end + 4),
43 | })
44 | }
45 |
46 | /// Serialize to BAM.
47 | pub fn to_buf(&self) -> Vec {
48 | Vec::new()
49 | }
50 | }
51 |
52 | #[derive(Debug)]
53 | struct AuxData {
54 | pub tag: [u8; 2],
55 | pub val_type: u8,
56 | /// corresponds to val_type.
57 | pub value: Vec, // todo: Placeholder
58 | }
59 |
60 | #[derive(Debug)]
61 | struct Alignment {
62 | /// Size of the entire alignment packet, *except the 4-byte block size*. Includes aux data.
63 | pub block_size: u32,
64 | pub ref_id: i32,
65 | pub pos: i32,
66 | pub mapq: u8,
67 | pub bin: u16,
68 | pub n_cigar_op: u16,
69 | pub flag: u16,
70 | pub next_ref_id: i32,
71 | pub next_pos: i32,
72 | pub tlen: i32,
73 | pub read_name: String,
74 | pub cigar: Vec,
75 | pub seq: Vec, // 4-bit encoded
76 | pub qual: String, // todo: Vec?
77 | pub aux_data: Vec,
78 | }
79 |
80 | impl Alignment {
81 | /// Deserialize from BAM.
82 | pub fn from_buf(buf: &[u8]) -> io::Result {
83 | if buf.len() < 36 {
84 | return Err(io::Error::new(
85 | ErrorKind::InvalidData,
86 | "Buffer is too small to contain a valid Alignment record",
87 | ));
88 | }
89 |
90 | let block_size = parse_le!(buf, u32, 0..4);
91 |
92 | println!("Block size: {:?}", block_size);
93 |
94 | let ref_id = parse_le!(buf, i32, 4..8);
95 | let pos = parse_le!(buf, i32, 8..12);
96 |
97 | let l_read_name = buf[12] as usize;
98 | let mapq = buf[13];
99 |
100 | let bin = parse_le!(buf, u16, 14..16);
101 | let n_cigar_op = parse_le!(buf, u16, 16..18);
102 | let flag = parse_le!(buf, u16, 18..20);
103 |
104 | let l_seq = parse_le!(buf, u32, 20..24) as usize;
105 |
106 | let next_ref_id = parse_le!(buf, i32, 24..28);
107 | let next_pos = parse_le!(buf, i32, 28..32);
108 | let tlen = parse_le!(buf, i32, 32..36);
109 |
110 | let mut i = 36;
111 | // -1: Ommit the trailing null.
112 | println!("Len read name: {:?}", l_read_name);
113 | let read_name = String::from_utf8(buf[i..i + l_read_name - 1].to_vec())
114 | .map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
115 | i += l_read_name;
116 |
117 | let cigar_len = n_cigar_op as usize;
118 | let mut cigar = Vec::new();
119 | for _ in 0..cigar_len {
120 | let cigar_op = parse_le!(buf, u32, i..i + 4);
121 | cigar.push(cigar_op);
122 | i += 4;
123 | }
124 |
125 | // todo: Figure out how this is actually handled, and set the struct type A/R.
126 | let seq_len = (l_seq + 1) / 2; // Each nucleotide is 4 bits
127 | let seq = buf[i..i + seq_len].to_vec();
128 | i += seq_len;
129 |
130 | let qual = buf[i..i + l_seq].to_vec();
131 | i += l_seq;
132 |
133 | let aux_data = Vec::new(); // todo: Parse auxiliary data from the remaining bytes.
134 |
135 | Ok(Self {
136 | block_size,
137 | ref_id,
138 | pos,
139 | mapq,
140 | bin,
141 | n_cigar_op,
142 | flag,
143 | next_ref_id,
144 | next_pos,
145 | tlen,
146 | read_name,
147 | cigar,
148 | seq,
149 | qual: String::new(), // Placeholder until auxiliary data parsing
150 | aux_data,
151 | })
152 | }
153 |
154 | /// Serialize to BAM.
155 | pub fn to_buf_(&self) -> Vec {
156 | // todo: If desired.
157 | Vec::new()
158 | }
159 | }
160 |
161 | #[derive(Debug)]
162 | /// Represents BAM or SAM data. See the spec document, section 4.2. This maps directly to the BAM format.
163 | /// Fields, and their types, here and in sub-structs are taken directly from this table.
164 | pub struct AlignmentMap {
165 | pub l_text: u32,
166 | pub text: String,
167 | pub n_ref: u32,
168 | pub refs: Vec,
169 | pub alignments: Vec,
170 | }
171 |
172 | impl AlignmentMap {
173 | /// Deserialize an alignment record from a buffer containing BAM data.
174 | pub fn from_buf(buf: &[u8]) -> io::Result {
175 | if buf.len() < 12 {
176 | return Err(io::Error::new(
177 | ErrorKind::InvalidData,
178 | "Buffer is too small to contain a valid header",
179 | ));
180 | }
181 |
182 | let l_text = parse_le!(buf, u32, 4..8);
183 | let text_end = 8 + l_text as usize;
184 | let n_ref = parse_le!(buf, u32, text_end..text_end + 4);
185 | let refs_start = text_end + 4;
186 |
187 | let mut i = refs_start;
188 | let mut refs = Vec::new();
189 | for _ in 0..n_ref {
190 | let ref_seq = RefSeq::from_buf(buf[i..].try_into().unwrap())?;
191 | i += 9 + ref_seq.name.len();
192 |
193 | refs.push(ref_seq);
194 | }
195 |
196 | let mut alignments = Vec::new();
197 |
198 | while i + 1 < buf.len() {
199 | let alignment = Alignment::from_buf(buf[i..].try_into().unwrap())?;
200 |
201 | println!("\n\n Alignment: {:?}", alignment);
202 |
203 | i += 4 + alignment.block_size as usize;
204 |
205 | alignments.push(alignment);
206 | }
207 |
208 | if buf[0..4] != MAGIC {
209 | return Err(io::Error::new(
210 | ErrorKind::InvalidData,
211 | "Incorrect BAM magic.".to_string(),
212 | ));
213 | }
214 |
215 | Ok(Self {
216 | l_text,
217 | // todo: Map the error; don't unwrap.
218 | text: String::from_utf8(buf[8..text_end].to_vec()).unwrap(),
219 | n_ref,
220 | refs,
221 | alignments,
222 | })
223 | }
224 |
225 | /// Serialize to BAM.
226 | pub fn to_buf(&self) -> Vec {
227 | Vec::new()
228 | }
229 | }
230 |
231 | pub fn import(path: &Path) -> io::Result {
232 | let file = File::open(path)?;
233 | let mut decoder = GzDecoder::new(file);
234 | // let mut decoder = MultiGzDecoder::new(file);
235 |
236 | println!("Decoding file...");
237 |
238 | let mut buf = Vec::new();
239 | // let mut buf = [0; 80_000];
240 | decoder.read_to_end(&mut buf)?;
241 |
242 | println!("Decode complete. Reading...");
243 |
244 | let header = AlignmentMap::from_buf(&buf);
245 |
246 | header
247 |
248 | // AlignmentMap::from_buf(&buf)
249 | }
250 |
251 | // pub fn import(path: &Path) -> io::Result {
252 | // let file = File::open(path)?;
253 | //
254 | // let mut reader =
255 | // BGZFReader::new(file).unwrap(); // todo: Map error.
256 | //
257 | // // let index = BGZFIndex::from_reader(reader)?;
258 | //
259 | // // println!("Index entries: {:?}", index.entries());
260 | //
261 | // // let mut reader =
262 | // // IndexedBGZFReader::new(reader, index).unwrap(); // todo: Map error.
263 | //
264 | // // println!("Voffset A: {:?}", reader.bgzf_pos());
265 | //
266 | // reader.bgzf_seek(0).unwrap(); // todo: Map error.
267 | // // reader.seek(0).unwrap(); // todo: Map error.
268 | //
269 | // // let mut buf = Vec::new();
270 | // let mut buf = vec![0; 80_000];
271 | //
272 | // // reader.read_to_end(&mut buf).unwrap(); // todo: Map error.
273 | // reader.read(&mut buf).unwrap(); // todo: Map error.
274 | //
275 | // println!("Voffset: {:?}", reader.bgzf_pos());
276 | //
277 | // println!("BUf contents: {:?}", &buf[13180..13220]);
278 | //
279 | // AlignmentMap::from_buf(&buf)
280 | // }
281 |
282 | // /// todo: Move to `file_io` A/R.
283 | // /// Note: BAM files are compressed using BGZF, and are therefore split into blocks no larger than 64kb.
284 | // pub fn import(path: &Path) -> io::Result {
285 | // let mut file = File::open(path)?;
286 | // let mut decompressed_data = Vec::new();
287 | //
288 | // // todo: Organize this A/R.
289 | //
290 | // loop {
291 | // println!("BAM block...\n");
292 | // // Read BGZF block header (18 bytes)
293 | // let mut block_header = [0u8; 18];
294 | // if let Err(e) = file.read_exact(&mut block_header) {
295 | // if e.kind() == ErrorKind::UnexpectedEof {
296 | // break; // Reached end of file
297 | // }
298 | // return Err(e);
299 | // }
300 | //
301 | // // Parse block size (last 2 bytes of header, little-endian)
302 | // let block_size = u16::from_le_bytes([block_header[16], block_header[17]]) as usize;
303 | //
304 | // println!("Block size: {:?}", block_size);
305 | //
306 | // if block_size < 18 {
307 | // return Err(io::Error::new(
308 | // ErrorKind::InvalidData,
309 | // "Invalid BGZF block size",
310 | // ));
311 | // }
312 | //
313 | // // Read the block data
314 | // let mut block_data = vec![0u8; block_size - 18];
315 | // file.read_exact(&mut block_data)?;
316 | //
317 | // // Decompress the block
318 | // let mut decoder = GzDecoder::new(&block_data[..]);
319 | // let mut decompressed_block = Vec::new();
320 | // decoder.read_to_end(&mut decompressed_block)?;
321 | //
322 | // // Append to the full decompressed buffer
323 | // decompressed_data.extend_from_slice(&decompressed_block);
324 | // }
325 | //
326 | // AlignmentMap::from_buf(&decompressed_data)
327 | // }
328 |
329 | /// Calculate bin given an alignment covering [beg, end) (zero-based, half-closed-half-open)
330 | /// This is adapted from C code in the spec document.
331 | fn reg_to_bin(beg: i32, end: i32) -> i32 {
332 | let end = end - 1; // Adjust end to be inclusive
333 | if beg >> 14 == end >> 14 {
334 | return ((1 << 15) - 1) / 7 + (beg >> 14);
335 | }
336 | if beg >> 17 == end >> 17 {
337 | return ((1 << 12) - 1) / 7 + (beg >> 17);
338 | }
339 | if beg >> 20 == end >> 20 {
340 | return ((1 << 9) - 1) / 7 + (beg >> 20);
341 | }
342 | if beg >> 23 == end >> 23 {
343 | return ((1 << 6) - 1) / 7 + (beg >> 23);
344 | }
345 | if beg >> 26 == end >> 26 {
346 | return ((1 << 3) - 1) / 7 + (beg >> 26);
347 | }
348 | 0
349 | }
350 |
351 | /// Calculate the list of bins that may overlap with region [beg, end) (zero-based)
352 | /// This is adapted from C code in the spec document.
353 | fn reg_to_bins(beg: i32, end: i32, list: &mut Vec) -> usize {
354 | let end = end - 1; // Adjust end to be inclusive
355 | list.push(0);
356 | for k in (1 + (beg >> 26))..=(1 + (end >> 26)) {
357 | list.push(k as u16);
358 | }
359 | for k in (9 + (beg >> 23))..=(9 + (end >> 23)) {
360 | list.push(k as u16);
361 | }
362 | for k in (73 + (beg >> 20))..=(73 + (end >> 20)) {
363 | list.push(k as u16);
364 | }
365 | for k in (585 + (beg >> 17))..=(585 + (end >> 17)) {
366 | list.push(k as u16);
367 | }
368 | for k in (4681 + (beg >> 14))..=(4681 + (end >> 14)) {
369 | list.push(k as u16);
370 | }
371 | list.len()
372 | }
373 |
--------------------------------------------------------------------------------
/src/external_websites.rs:
--------------------------------------------------------------------------------
1 | //! For opening the browser to NCBI BLAST, PDB etc.
2 | //!
3 | //! PDB Search API: https://search.rcsb.org/#search-api
4 | //! PDB Data API: https://data.rcsb.org/#data-api
5 |
6 | use bio_apis::ncbi;
7 |
8 | use crate::{Selection, state::State};
9 |
10 | /// BLAST the selected Feature, primer, or selection. Prioritize the selection.
11 | /// This function handles extracting the sequence to BLAST from possible selections.
12 | pub fn blast(state: &State) {
13 | let data = &state.generic[state.active];
14 |
15 | let val = match state.ui.text_selection {
16 | Some(sel) => {
17 | // Don't format sel directly, as we insert the bp count downstream for use with feature selections.
18 | Some((
19 | sel.index_seq(&data.seq),
20 | format!("{}, {}..{}", data.metadata.plasmid_name, sel.start, sel.end),
21 | ))
22 | }
23 | None => match state.ui.selected_item {
24 | Selection::Feature(feat_i) => {
25 | if feat_i >= data.features.len() {
26 | eprintln!("Invalid selected feature");
27 | None
28 | } else {
29 | let feature = &data.features[feat_i];
30 | Some((feature.range.index_seq(&data.seq), feature.label.clone()))
31 | }
32 | }
33 | Selection::Primer(prim_i) => {
34 | if prim_i >= data.primers.len() {
35 | eprintln!("Invalid selected primer");
36 | None
37 | } else {
38 | let primer = &data.primers[prim_i];
39 | Some((Some(&primer.sequence[..]), primer.name.clone()))
40 | }
41 | }
42 | Selection::None => None,
43 | },
44 | };
45 |
46 | // todo: Handle reverse.
47 |
48 | if let Some((seq, name)) = val {
49 | if let Some(s) = seq {
50 | ncbi::open_blast(s, &name);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/feature_db_builder.rs:
--------------------------------------------------------------------------------
1 | //! This is the entrypoint for a standalone program that parse features from GenBank and SnapGene files.
2 | //! It assigns each sequence a label and feature type.
3 |
4 | use std::{
5 | fs,
6 | path::{Path, PathBuf},
7 | str::FromStr,
8 | };
9 |
10 | mod file_io;
11 | mod gui;
12 | mod primer;
13 | mod sequence; // Required due to naviation::Page being in file_io::save
14 |
15 | // mod main; // todo temp
16 | mod feature_db_load; // todo temp
17 |
18 | use sequence::{FeatureType, Seq};
19 |
20 | use crate::file_io::genbank;
21 |
22 | const SOURCE_DIR: &str = "data";
23 |
24 | // struct FeatureMapItem {
25 | // feature_name: String,
26 | // feature_type: FeatureType,
27 | // seq: Seq,
28 | // }
29 |
30 | fn collect_files_in_directory(dir: &Path) -> Vec {
31 | let mut files = Vec::new();
32 |
33 | if dir.is_dir() {
34 | for entry in fs::read_dir(dir).unwrap() {
35 | let path = entry.unwrap().path();
36 | if path.is_file() {
37 | files.push(path);
38 | }
39 | }
40 | }
41 |
42 | files
43 | }
44 |
45 | /// Combine feature maps that have identical sequences.
46 | fn consolidate(feature_map: &[FeatureMapItem]) -> Vec {
47 | let mut result = Vec::new();
48 |
49 | result
50 | }
51 |
52 | fn main() {
53 | // todo: all files
54 | let files_genbank = collect_files_in_directory(&PathBuf::from_str(SOURCE_DIR).unwrap());
55 | let files_snapgene = [];
56 |
57 | let mut feature_maps = Vec::new();
58 |
59 | for file in files_genbank {
60 | let path = PathBuf::from_str(file).unwrap();
61 |
62 | if let Ok(data) = genbank::import_genbank(&path) {
63 | for feature in &data.features {
64 | feature_maps.push({
65 | FeatureMapItem {
66 | name: feature.name.clone(),
67 | feature_type: feature.feature_type,
68 | seq: data.seq[feature.range.0 - 1..feature.range.1],
69 | }
70 | })
71 | }
72 | }
73 | }
74 |
75 | // todo: SQlite DB?
76 | }
77 |
--------------------------------------------------------------------------------
/src/file_io/mod.rs:
--------------------------------------------------------------------------------
1 | //! This module contains code for saving and loading in several file formats.
2 |
3 | use std::{path::Path, sync::Arc};
4 |
5 | use egui_file_dialog::{FileDialog, FileDialogConfig};
6 | use na_seq::{Seq, SeqTopology};
7 |
8 | use crate::{
9 | file_io::save::{DEFAULT_DNA_FILE, DEFAULT_FASTA_FILE, DEFAULT_GENBANK_FILE, QUICKSAVE_FILE},
10 | misc_types::{Feature, Metadata},
11 | primer::Primer,
12 | };
13 |
14 | pub mod ab1;
15 | mod ab1_tags;
16 | pub mod genbank;
17 | mod pcad;
18 | pub mod save;
19 | pub mod snapgene;
20 |
21 | /// The most important data to store, used by our format, GenBank, and SnapGene.
22 | /// We use this in our main State struct to keep track of this data.
23 | #[derive(Default, Clone)]
24 | pub struct GenericData {
25 | pub seq: Seq,
26 | pub topology: SeqTopology,
27 | pub features: Vec,
28 | pub primers: Vec,
29 | pub metadata: Metadata,
30 | }
31 |
32 | pub struct FileDialogs {
33 | pub save: FileDialog,
34 | pub load: FileDialog,
35 | pub export_fasta: FileDialog,
36 | pub export_genbank: FileDialog,
37 | pub export_dna: FileDialog,
38 | pub cloning_load: FileDialog,
39 | }
40 |
41 | impl Default for FileDialogs {
42 | fn default() -> Self {
43 | // We can't clone `FileDialog`; use this to reduce repetition instead.
44 | let cfg_import = FileDialogConfig {
45 | // todo: Explore other optiosn A/R
46 | ..Default::default()
47 | }
48 | .add_file_filter_extensions("PlasCAD", vec!["pcad"])
49 | .add_file_filter_extensions("FASTA", vec!["fasta"])
50 | .add_file_filter_extensions("GenBank", vec!["gb, gbk"])
51 | .add_file_filter_extensions("SnapGene DNA", vec!["dna"])
52 | .add_file_filter_extensions(
53 | "PCAD/FASTA/GB/DNA/AB1",
54 | vec!["pcad", "fasta, gb, gbk, dna, ab1"],
55 | );
56 |
57 | let save = FileDialog::new()
58 | // .add_quick_access("Project", |s| {
59 | // s.add_path("☆ Examples", "examples");
60 | // })
61 | .add_save_extension("PlasCAD", "pcad")
62 | .default_save_extension("PlasCAD")
63 | .default_file_name(QUICKSAVE_FILE);
64 | // .id("0");
65 |
66 | let import = FileDialog::with_config(cfg_import.clone())
67 | .default_file_filter("PCAD/FASTA/GB/DNA/AB1");
68 |
69 | let export_fasta = FileDialog::new()
70 | .add_save_extension("FASTA", "fasta")
71 | .default_save_extension("FASTA")
72 | .default_file_name(DEFAULT_FASTA_FILE);
73 |
74 | let export_genbank = FileDialog::new()
75 | .add_save_extension("GenBank", "gb")
76 | .default_save_extension("GenBank")
77 | .default_file_name(DEFAULT_GENBANK_FILE);
78 |
79 | let export_dna = FileDialog::new()
80 | .add_save_extension("SnapGene DNA", "dna")
81 | .default_save_extension("SnapGene DNA")
82 | .default_file_name(DEFAULT_DNA_FILE);
83 |
84 | let cloning_import =
85 | FileDialog::with_config(cfg_import).default_file_filter("PCAD/FASTA/GB/DNA/AB1");
86 |
87 | Self {
88 | save,
89 | // load: load_,
90 | load: import,
91 | export_fasta,
92 | export_genbank,
93 | export_dna,
94 | cloning_load: cloning_import,
95 | // selected: None,
96 | }
97 | }
98 | }
99 |
100 | /// There doesn't seem to be a clear name in GenBank or Snapgene formats; use the filename.
101 | /// Note: This includes error checking, but this should always pass under normal circumstances.
102 | fn get_filename(path: &Path) -> String {
103 | if let Some(file_name) = path.file_stem() {
104 | file_name
105 | .to_str()
106 | .map(|s| s.to_string())
107 | .unwrap_or_default()
108 | } else {
109 | String::new()
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/file_io/pcad.rs:
--------------------------------------------------------------------------------
1 | //! This module contains code for saving and loading in our own PCAD format.
2 | //!
3 | //! This is a binary format that uses packets for each of several message types. This sytem
4 | //! should have better backwards compatibility than raw serialization and deserialization using
5 | //! Bincode or similar.
6 | //!
7 | //! Byte encoding is big endian.
8 |
9 | use std::{io, io::ErrorKind};
10 |
11 | use bincode::config;
12 | use na_seq::{deser_seq_bin, serialize_seq_bin};
13 | use num_enum::TryFromPrimitive;
14 |
15 | use crate::file_io::save::StateToSave;
16 |
17 | const START_BYTES: [u8; 2] = [0xca, 0xfe]; // Arbitrary, used as a sanity check.
18 | const PACKET_START: u8 = 0x11;
19 | const PACKET_OVERHEAD: usize = 6; // packet start, packet type, message size.
20 |
21 | #[repr(u8)]
22 | #[derive(Clone, Copy, PartialEq, TryFromPrimitive)]
23 | pub enum PacketType {
24 | Sequence = 0,
25 | Features = 1,
26 | Primers = 2,
27 | Metadata = 3,
28 | // IonConcentrations = 6,
29 | Portions = 7,
30 | // PathLoaded = 10,
31 | Topology = 11,
32 | Ab1 = 12,
33 | }
34 |
35 | /// Byte 0: Standard packet start. Bytes 1-4: u32 of payload len. Bytes 5[..]: Payload.
36 | pub struct Packet {
37 | type_: PacketType,
38 | payload: Vec,
39 | }
40 |
41 | impl Packet {
42 | /// Note: Bytes includes this payload, and potentially until the end of the entire file data.
43 | /// We use the length bytes to know when to stop reading.
44 | pub fn from_bytes(bytes: &[u8]) -> io::Result {
45 | if bytes.len() < PACKET_OVERHEAD {
46 | return Err(io::Error::new(
47 | ErrorKind::InvalidData,
48 | "Packet must be at least 2 bytes",
49 | ));
50 | }
51 |
52 | if bytes[0] != PACKET_START {
53 | return Err(io::Error::new(
54 | ErrorKind::InvalidData,
55 | "Invalid packet start byte in PCAD file.",
56 | ));
57 | }
58 |
59 | // todo: This may not be necessary, due to the vec being passed.
60 | let payload_size = u32::from_be_bytes(bytes[1..5].try_into().unwrap()) as usize;
61 | if bytes.len() < PACKET_OVERHEAD + payload_size {
62 | return Err(io::Error::new(
63 | ErrorKind::InvalidData,
64 | "Remaining payload is too short based on read packet len in PCAD file.",
65 | ));
66 | }
67 |
68 | Ok(Self {
69 | type_: bytes[5].try_into().map_err(|_| {
70 | io::Error::new(ErrorKind::InvalidData, "Invalid packet type received")
71 | })?,
72 | payload: bytes[PACKET_OVERHEAD..PACKET_OVERHEAD + payload_size].to_vec(), // todo: This essentially clones? Not ideal.
73 | })
74 | }
75 |
76 | pub fn to_bytes(&self) -> Vec {
77 | let mut result = vec![PACKET_START];
78 |
79 | let len = self.payload.len() as u32;
80 | result.extend(&len.to_be_bytes());
81 |
82 | result.push(self.type_ as u8);
83 | result.extend(&self.payload);
84 |
85 | result
86 | }
87 | }
88 |
89 | impl StateToSave {
90 | /// Serialize state as bytes in the PCAD format, e.g. for file saving.
91 | pub fn to_bytes(&self) -> Vec {
92 | let cfg = config::standard();
93 | let mut result = Vec::new();
94 |
95 | result.extend(&START_BYTES);
96 |
97 | // Note: The order we add these packets in doesn't make a difference for loading.
98 |
99 | let seq_packet = Packet {
100 | type_: PacketType::Sequence,
101 | payload: serialize_seq_bin(&self.generic.seq),
102 | };
103 |
104 | let features_packet = Packet {
105 | type_: PacketType::Features,
106 | payload: bincode::encode_to_vec(&self.generic.features, cfg).unwrap(),
107 | };
108 |
109 | let primers_packet = Packet {
110 | type_: PacketType::Primers,
111 | payload: bincode::encode_to_vec(&self.generic.primers, cfg).unwrap(),
112 | };
113 |
114 | let metadata_packet = Packet {
115 | type_: PacketType::Metadata,
116 | payload: bincode::encode_to_vec(&self.generic.metadata, cfg).unwrap(),
117 | };
118 |
119 | let topology_packet = Packet {
120 | type_: PacketType::Topology,
121 | payload: bincode::encode_to_vec(&self.generic.topology, cfg).unwrap(),
122 | };
123 |
124 | // let ion_concentrations_packet = Packet {
125 | // type_: PacketType::IonConcentrations,
126 | // payload: bincode::encode_to_vec(&self.ion_concentrations, cfg).unwrap(),
127 | // };
128 |
129 | let portions_packet = Packet {
130 | type_: PacketType::Portions,
131 | payload: bincode::encode_to_vec(&self.portions, cfg).unwrap(),
132 | };
133 |
134 | // let path_loaded_packet = Packet {
135 | // type_: PacketType::PathLoaded,
136 | // payload: bincode::encode_to_vec(&self.path_loaded, cfg).unwrap(),
137 | // };
138 |
139 | let ab1_packet = Packet {
140 | type_: PacketType::Ab1,
141 | payload: bincode::encode_to_vec(&self.ab1_data, cfg).unwrap(),
142 | };
143 |
144 | result.extend(&seq_packet.to_bytes());
145 | result.extend(&features_packet.to_bytes());
146 | result.extend(&primers_packet.to_bytes());
147 | result.extend(&metadata_packet.to_bytes());
148 | result.extend(&topology_packet.to_bytes());
149 | result.extend(&ab1_packet.to_bytes());
150 |
151 | // result.extend(&ion_concentrations_packet.to_bytes());
152 | result.extend(&portions_packet.to_bytes());
153 | // result.extend(&path_loaded_packet.to_bytes());
154 |
155 | result
156 | }
157 |
158 | /// Deserialize state as bytes in the PCAD format, e.g. for file loading.
159 | pub fn from_bytes(bytes: &[u8]) -> io::Result {
160 | if bytes[0..2] != START_BYTES {
161 | return Err(io::Error::new(
162 | ErrorKind::InvalidData,
163 | "Invalid start bytes in PCAD file.",
164 | ));
165 | }
166 |
167 | let cfg = config::standard();
168 | let mut result = StateToSave::default();
169 |
170 | let mut i = START_BYTES.len();
171 | loop {
172 | if i + PACKET_OVERHEAD > bytes.len() {
173 | break; // End of the packet.
174 | }
175 |
176 | let bytes_remaining = &bytes[i..];
177 | let packet = match Packet::from_bytes(bytes_remaining) {
178 | Ok(p) => p,
179 | Err(e) => {
180 | eprintln!("Problem opening a packet: {:?}", e);
181 | let payload_size =
182 | u32::from_be_bytes(bytes_remaining[1..5].try_into().unwrap()) as usize;
183 | i += PACKET_OVERHEAD + payload_size;
184 | continue;
185 | }
186 | };
187 |
188 | i += PACKET_OVERHEAD + packet.payload.len();
189 |
190 | // Now, add packet data to our result A/R.
191 | match packet.type_ {
192 | PacketType::Sequence => match deser_seq_bin(&packet.payload) {
193 | Ok(v) => result.generic.seq = v,
194 | Err(e) => eprintln!("Error decoding sequence packet: {e}"),
195 | },
196 | PacketType::Features => match bincode::decode_from_slice(&packet.payload, cfg) {
197 | Ok(v) => result.generic.features = v.0,
198 | Err(e) => eprintln!("Error decoding features packet: {e}"),
199 | },
200 | PacketType::Primers => match bincode::decode_from_slice(&packet.payload, cfg) {
201 | Ok(v) => result.generic.primers = v.0,
202 | Err(e) => eprintln!("Error decoding primers packet: {e}"),
203 | },
204 | PacketType::Metadata => match bincode::decode_from_slice(&packet.payload, cfg) {
205 | Ok(v) => result.generic.metadata = v.0,
206 | Err(e) => eprintln!("Error decoding metadata packet: {e}"),
207 | },
208 | PacketType::Topology => match bincode::decode_from_slice(&packet.payload, cfg) {
209 | Ok(v) => result.generic.topology = v.0,
210 | Err(e) => eprintln!("Error decoding topology packet: {e}"),
211 | },
212 | // PacketType::IonConcentrations => {
213 | // match bincode::decode_from_slice(&packet.payload, cfg) {
214 | // Ok(v) => result.ion_concentrations = v.0,
215 | // Err(e) => eprintln!("Error decoding ion concentrations packet: {e}"),
216 | // }
217 | // }
218 | PacketType::Portions => match bincode::decode_from_slice(&packet.payload, cfg) {
219 | Ok(v) => result.portions = v.0,
220 | Err(e) => eprintln!("Error decoding portions packet: {e}"),
221 | },
222 | PacketType::Ab1 => match bincode::decode_from_slice(&packet.payload, cfg) {
223 | Ok(v) => result.ab1_data = v.0,
224 | Err(e) => eprintln!("Error decoding AB1 packet: {e}"),
225 | },
226 | // PacketType::PathLoaded => match bincode::decode_from_slice(&packet.payload, cfg) {
227 | // Ok(v) => result.path_loaded = v.0,
228 | // Err(e) => eprintln!("Error decoding Seq packet: {e}"),
229 | // },
230 | }
231 | }
232 |
233 | Ok(result)
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/gui/ab1.rs:
--------------------------------------------------------------------------------
1 | //! Contains code for viewing AB1 sequencing data, e.g. from Sanger sequencing.
2 |
3 | use copypasta::{ClipboardContext, ClipboardProvider};
4 | use eframe::{
5 | egui::{
6 | Align2, Color32, FontFamily, FontId, Frame, Pos2, Rect, RichText, Sense, Shape, Slider,
7 | Stroke, Ui, pos2, vec2,
8 | },
9 | emath::RectTransform,
10 | epaint::PathShape,
11 | };
12 | use na_seq::{Nucleotide, seq_to_str_lower};
13 |
14 | use crate::{
15 | ab1::SeqRecordAb1,
16 | feature_db_load::find_features,
17 | file_io::GenericData,
18 | gui::{BACKGROUND_COLOR, COL_SPACING, ROW_SPACING},
19 | misc_types::Metadata,
20 | state::State,
21 | util::merge_feature_sets,
22 | };
23 |
24 | const NT_COLOR: Color32 = Color32::from_rgb(180, 220, 220);
25 | const NT_WIDTH: f32 = 8.; // pixels
26 | const STROKE_WIDTH_PEAK: f32 = 1.;
27 | const PEAK_WIDTH_DIV2: f32 = 2.;
28 |
29 | // Peak heights are normallized, so that the maximum value is this.
30 | const PEAK_MAX_HEIGHT: f32 = 120.;
31 |
32 | const COLOR_A: Color32 = Color32::from_rgb(20, 220, 20);
33 | const COLOR_C: Color32 = Color32::from_rgb(130, 130, 255);
34 | const COLOR_T: Color32 = Color32::from_rgb(255, 100, 100);
35 | const COLOR_G: Color32 = Color32::from_rgb(200, 200, 200);
36 |
37 | /// This mapping is based off conventions in other software.
38 | fn nt_color_map(nt: Nucleotide) -> Color32 {
39 | match nt {
40 | Nucleotide::A => COLOR_A,
41 | Nucleotide::C => COLOR_C,
42 | Nucleotide::T => COLOR_T,
43 | Nucleotide::G => COLOR_G,
44 | }
45 | }
46 |
47 | /// Map sequence index to a horizontal pixel.
48 | fn index_to_posit(i: usize, num_nts: usize, ui: &Ui) -> f32 {
49 | // todo: Cap if width too small for nt len. Varying zoom, etc.
50 | // let width = ui.available_width();
51 | //
52 | // let num_nts_disp = width / NT_WIDTH;
53 |
54 | // let scale_factor = width / num_nts as f32;
55 | i as f32 * NT_WIDTH
56 | }
57 |
58 | /// Plot the peaks and confidence values; draw letters
59 | fn plot(data: &SeqRecordAb1, to_screen: &RectTransform, start_i: usize, ui: &mut Ui) -> Vec {
60 | let mut result = Vec::new();
61 |
62 | let nt_y = 160.;
63 | let plot_y = nt_y - 20.;
64 |
65 | // todo: Don't run this calc every time.
66 | let data_scaler = {
67 | let mut max_peak = 0;
68 | for i in 0..data.data_ch1.len() {
69 | if i > 40 {
70 | // todo: Sloppy performance saver.
71 | continue;
72 | }
73 |
74 | let ch1 = data.data_ch1[i];
75 | let ch2 = data.data_ch2[i];
76 | let ch3 = data.data_ch3[i];
77 | let ch4 = data.data_ch4[i];
78 |
79 | for ch in [ch1, ch2, ch3, ch4] {
80 | if ch > max_peak {
81 | max_peak = ch;
82 | }
83 | }
84 | }
85 |
86 | PEAK_MAX_HEIGHT / max_peak as f32
87 | };
88 |
89 | let width = ui.available_width();
90 | let num_nts_disp = width / NT_WIDTH;
91 |
92 | // Display nucleotides and quality values.
93 | for i_pos in 0..num_nts_disp as usize {
94 | let i = start_i + i_pos;
95 | if data.sequence.is_empty() || i > data.sequence.len() - 1 {
96 | break;
97 | }
98 |
99 | let nt = data.sequence[i];
100 |
101 | let x_pos = index_to_posit(i_pos, data.sequence.len(), ui);
102 |
103 | if x_pos > ui.available_width() - 15. {
104 | continue; // todo: QC the details, if you need to_screen here etc.
105 | }
106 |
107 | let nt_color = nt_color_map(nt);
108 |
109 | result.push(ui.ctx().fonts(|fonts| {
110 | Shape::text(
111 | fonts,
112 | to_screen * pos2(x_pos, nt_y),
113 | Align2::CENTER_CENTER,
114 | &nt.to_str_lower(),
115 | FontId::new(12., FontFamily::Monospace),
116 | nt_color,
117 | )
118 | }));
119 |
120 | // todo: Display quality values below.
121 | // let quality = data.quality[i]; // todo: Index check.
122 | }
123 |
124 | // Display data.
125 | for i_pos in 0..num_nts_disp as usize * 4 {
126 | let i = start_i * 4 + i_pos;
127 | if data.data_ch1.is_empty() || i > data.data_ch1.len() - 1 {
128 | break;
129 | }
130 |
131 | let x_pos = index_to_posit(i_pos, data.sequence.len(), ui) / 4.;
132 | if x_pos > ui.available_width() - 15. {
133 | continue; // todo: QC the details, if you need to_screen here etc.
134 | }
135 |
136 | let ch1 = data.data_ch1[i];
137 | let ch2 = data.data_ch2[i];
138 | let ch3 = data.data_ch3[i];
139 | let ch4 = data.data_ch4[i];
140 | // todo: Index error handling.
141 |
142 | for (ch, color) in [
143 | (ch1, COLOR_G),
144 | (ch2, COLOR_A),
145 | (ch3, COLOR_T),
146 | (ch4, COLOR_C),
147 | ] {
148 | let stroke = Stroke::new(STROKE_WIDTH_PEAK, color);
149 |
150 | // todo: Autoscale height
151 | let base_pos = pos2(x_pos, plot_y);
152 |
153 | // todo: These may get too thin.
154 | let top_left = base_pos + vec2(-PEAK_WIDTH_DIV2, ch as f32 * -data_scaler);
155 | let top_right = base_pos + vec2(PEAK_WIDTH_DIV2, ch as f32 * -data_scaler);
156 | let bottom_left = base_pos + vec2(-PEAK_WIDTH_DIV2, 0.);
157 | let bottom_right = base_pos + vec2(PEAK_WIDTH_DIV2, 0.);
158 |
159 | result.push(ui.ctx().fonts(|fonts| {
160 | // Shape::Path(PathShape::convex_polygon(
161 | Shape::Path(PathShape::closed_line(
162 | vec![
163 | to_screen * top_left,
164 | to_screen * bottom_left,
165 | to_screen * bottom_right,
166 | to_screen * top_right,
167 | ],
168 | // color,
169 | stroke,
170 | ))
171 | }));
172 | }
173 | }
174 |
175 | result
176 | }
177 |
178 | pub fn ab1_page(state: &mut State, ui: &mut Ui) {
179 | ui.horizontal(|ui| {
180 | let data = &state.ab1_data[state.active];
181 |
182 | ui.heading("AB1 sequencing view");
183 |
184 | ui.add_space(COL_SPACING * 2.);
185 |
186 | if ui.button(RichText::new("🗐 Copy sequence")).on_hover_text("Copy this sequence to the clipboard.").clicked() {
187 | let mut ctx = ClipboardContext::new().unwrap();
188 | ctx.set_contents(seq_to_str_lower(&data.sequence)).unwrap();
189 | }
190 |
191 | ui.add_space(COL_SPACING);
192 |
193 | if ui
194 | .button("➕ Create data (e.g. Genbank, PCAD etc) as a new tab.")
195 | .on_hover_text("Create a new non-AB1 data set\
196 | that may include features, primers, etc. May be edited, and saved to Genbank, PCAD, or SnapGene formats.")
197 | .clicked()
198 | {
199 | // Note: This segment is almost a duplicate of `State::add_tab` and the similar section in `cloning`.
200 | let plasmid_name = match &state.tabs_open[state.active].path {
201 | Some(p) => p.file_name().unwrap().to_str().unwrap_or_default(),
202 | None => "Plasmid from AB1", // This shouldn't happen, I believe.
203 | }.to_owned().replace(".ab1", "");
204 |
205 | let generic = GenericData {
206 | seq: data.sequence.clone(),
207 | metadata: Metadata {
208 | plasmid_name,
209 | ..Default::default()
210 | },
211 | ..Default::default()
212 | };
213 |
214 |
215 | state.generic.push(generic);
216 |
217 | state.portions.push(Default::default());
218 | state.volatile.push(Default::default());
219 | state.tabs_open.push(Default::default());
220 | state.ab1_data.push(Default::default());
221 |
222 | state.active = state.generic.len() - 1;
223 |
224 | // state.sync_seq_related(None);
225 |
226 | // Annotate. Don't add duplicates.
227 | let features = find_features(&state.get_seq());
228 | merge_feature_sets(&mut state.generic[state.active].features, &features)
229 |
230 | }
231 | });
232 | ui.add_space(ROW_SPACING / 2.);
233 |
234 | let data = &state.ab1_data[state.active];
235 | let width = ui.available_width();
236 | let num_nts_disp = width / NT_WIDTH;
237 | ui.spacing_mut().slider_width = width - 60.;
238 |
239 | let slider_max = {
240 | let v = data.sequence.len() as isize - num_nts_disp as isize + 1;
241 | if v > 0 { v as usize } else { 0 }
242 | };
243 |
244 | ui.add(Slider::new(&mut state.ui.ab1_start_i, 0..=slider_max));
245 |
246 | ui.add_space(ROW_SPACING / 2.);
247 |
248 | let mut shapes = Vec::new();
249 |
250 | Frame::canvas(ui.style())
251 | .fill(BACKGROUND_COLOR)
252 | .show(ui, |ui| {
253 | let data = &state.ab1_data[state.active];
254 |
255 | let (response, _painter) = {
256 | let desired_size = vec2(ui.available_width(), ui.available_height());
257 | ui.allocate_painter(desired_size, Sense::click())
258 | };
259 |
260 | let to_screen = RectTransform::from_to(
261 | // Rect::from_min_size(pos2(0., -VERITICAL_CIRCLE_OFFSET), response.rect.size()),
262 | Rect::from_min_size(Pos2::ZERO, response.rect.size()),
263 | response.rect,
264 | );
265 |
266 | let rect_size = response.rect.size();
267 |
268 | shapes.append(&mut plot(data, &to_screen, state.ui.ab1_start_i, ui));
269 |
270 | ui.painter().extend(shapes);
271 | });
272 | }
273 |
--------------------------------------------------------------------------------
/src/gui/alignment.rs:
--------------------------------------------------------------------------------
1 | use eframe::egui::{Color32, FontFamily, FontId, RichText, ScrollArea, TextEdit, Ui};
2 | use na_seq::{seq_aa_from_str, seq_aa_to_str, seq_from_str, seq_to_str_lower};
3 |
4 | use crate::{
5 | alignment::{AlignmentMode, align_pairwise_aa, align_pairwise_nt, distance_aa, distance_nt},
6 | gui::{
7 | COL_SPACING, ROW_SPACING,
8 | theme::{COLOR_ACTION, COLOR_INFO},
9 | },
10 | state::State,
11 | };
12 |
13 | fn mode_btn(state: &mut State, mode: AlignmentMode, name: &str, ui: &mut Ui) {
14 | let color = if state.alignment.mode == mode {
15 | Color32::LIGHT_BLUE
16 | } else {
17 | Color32::WHITE
18 | };
19 | if ui.button(RichText::new(name).color(color)).clicked() {
20 | state.alignment.mode = mode;
21 |
22 | // Reset fields to prevent invalid data.
23 | state.alignment.seq_a = Vec::new();
24 | state.alignment.seq_aa_a = Vec::new();
25 | state.alignment.seq_a_input = String::new();
26 | state.alignment.seq_b = Vec::new();
27 | state.alignment.seq_aa_b = Vec::new();
28 | state.alignment.seq_b_input = String::new();
29 | }
30 | }
31 |
32 | fn input_area(state: &mut State, seq_b: bool, ui: &mut Ui) {
33 | let (seq, seq_aa, seq_input) = if seq_b {
34 | (
35 | &mut state.alignment.seq_a,
36 | &mut state.alignment.seq_aa_a,
37 | &mut state.alignment.seq_a_input,
38 | )
39 | } else {
40 | (
41 | &mut state.alignment.seq_b,
42 | &mut state.alignment.seq_aa_b,
43 | &mut state.alignment.seq_b_input,
44 | )
45 | };
46 |
47 | let active_seq = state.generic[state.active].seq.clone(); // Avoid borrowing state in the closure
48 |
49 | ui.horizontal(|ui| {
50 | ui.label(if seq_b { "Seq B" } else { "Seq A" });
51 | ui.add_space(COL_SPACING);
52 |
53 | if let AlignmentMode::Dna = state.alignment.mode {
54 | if ui
55 | .button(RichText::new("Load active tab's sequence").color(Color32::LIGHT_BLUE))
56 | .clicked()
57 | {
58 | *seq = active_seq.clone(); // Use the pre-fetched active sequence
59 | *seq_input = seq_to_str_lower(seq);
60 | }
61 | }
62 | });
63 |
64 | let response = ui.add(TextEdit::multiline(seq_input).desired_width(800.));
65 | if response.changed() {
66 | match state.alignment.mode {
67 | AlignmentMode::Dna => {
68 | *seq = seq_from_str(seq_input);
69 | *seq_input = seq_to_str_lower(seq);
70 | // Todo: why?
71 | // state.sync_seq_related(None);
72 | }
73 | AlignmentMode::AminoAcid => {
74 | *seq_aa = seq_aa_from_str(seq_input);
75 | *seq_input = seq_aa_to_str(seq_aa);
76 | }
77 | }
78 | }
79 | }
80 |
81 | pub fn alignment_page(state: &mut State, ui: &mut Ui) {
82 | ui.add_space(ROW_SPACING);
83 |
84 | ui.horizontal(|ui| {
85 | ui.heading("Alignment (Work in Progress)");
86 |
87 | ui.add_space(COL_SPACING * 2.);
88 |
89 | mode_btn(state, AlignmentMode::Dna, "DNA", ui);
90 | mode_btn(state, AlignmentMode::AminoAcid, "Amino acid", ui);
91 | });
92 | ui.add_space(ROW_SPACING);
93 |
94 | ScrollArea::vertical().id_salt(200).show(ui, |ui| {
95 | input_area(state, false, ui);
96 | ui.add_space(ROW_SPACING);
97 | input_area(state, true, ui);
98 |
99 | ui.add_space(ROW_SPACING);
100 |
101 | if ui
102 | .button(RichText::new("Align").color(COLOR_ACTION))
103 | .clicked()
104 | {
105 | let alignment = match state.alignment.mode {
106 | AlignmentMode::Dna => {
107 | align_pairwise_nt(&state.alignment.seq_a, &state.alignment.seq_b)
108 | }
109 | AlignmentMode::AminoAcid => {
110 | align_pairwise_aa(&state.alignment.seq_aa_a, &state.alignment.seq_aa_b)
111 | }
112 | };
113 |
114 | state.alignment.alignment_result = Some(alignment.0);
115 | state.alignment.text_display = alignment.1;
116 |
117 | state.alignment.dist_result = Some(match state.alignment.mode {
118 | AlignmentMode::Dna => distance_nt(&state.alignment.seq_a, &state.alignment.seq_b),
119 | AlignmentMode::AminoAcid => {
120 | distance_aa(&state.alignment.seq_aa_a, &state.alignment.seq_aa_b)
121 | }
122 | });
123 | }
124 |
125 | ui.add_space(ROW_SPACING);
126 | if let Some(alignment) = &state.alignment.alignment_result {
127 | ui.heading(&format!("Alignment score: {:?}", alignment.score));
128 |
129 | ui.add_space(ROW_SPACING);
130 |
131 | ui.label(
132 | RichText::new(&state.alignment.text_display)
133 | .color(COLOR_INFO)
134 | .font(FontId::new(16., FontFamily::Monospace)),
135 | );
136 | }
137 |
138 | if let Some(dist) = &state.alignment.dist_result {
139 | ui.horizontal(|ui| {
140 | ui.heading(&format!("Distance score: {:?}", dist));
141 | let dist_type_text = if state.alignment.seq_a.len() == state.alignment.seq_b.len() {
142 | "(Hamming)"
143 | } else {
144 | "(Levenshtein)"
145 | };
146 | ui.label(dist_type_text);
147 | });
148 | }
149 | });
150 | }
151 |
--------------------------------------------------------------------------------
/src/gui/feature_table.rs:
--------------------------------------------------------------------------------
1 | //! GUI code for the features editor and related.
2 |
3 | use eframe::egui::{
4 | Color32, ComboBox, CursorIcon, Frame, RichText, ScrollArea, Stroke, TextEdit, Ui,
5 | };
6 |
7 | use crate::{
8 | Color, Selection,
9 | gui::{COL_SPACING, ROW_SPACING, int_field, theme::COLOR_ACTION},
10 | misc_types::{
11 | Feature,
12 | FeatureDirection::{self, Forward, Reverse},
13 | FeatureType,
14 | },
15 | state::State,
16 | util::RangeIncl,
17 | };
18 |
19 | const LABEL_EDIT_WIDTH: f32 = 140.;
20 |
21 | /// A color selector for use with feature addition and editing.
22 | fn color_picker(val: &mut Option, feature_color: Color, ui: &mut Ui) {
23 | let mut color_override = val.is_some();
24 | if ui.checkbox(&mut color_override, "").changed() {
25 | if color_override {
26 | // Default to the feature color when checking.
27 | *val = Some(feature_color);
28 | } else {
29 | *val = None;
30 | return;
31 | }
32 | }
33 |
34 | // Only show the color picker if choosing to override.
35 | if val.is_none() {
36 | return;
37 | }
38 |
39 | let (r, g, b) = val.unwrap();
40 | let mut color = Color32::from_rgb(r, g, b);
41 | if ui.color_edit_button_srgba(&mut color).changed() {
42 | *val = Some((color.r(), color.g(), color.b()));
43 | }
44 | }
45 |
46 | /// A selector for use with feature addition and editing.
47 | /// todo: Generic selector creator?
48 | pub fn direction_picker(val: &mut FeatureDirection, id: usize, ui: &mut Ui) {
49 | ComboBox::from_id_salt(id)
50 | .width(74.)
51 | .selected_text(val.to_string())
52 | .show_ui(ui, |ui| {
53 | for dir in [FeatureDirection::None, Forward, Reverse] {
54 | ui.selectable_value(val, dir, dir.to_string());
55 | }
56 | });
57 | }
58 |
59 | /// A selector for use with feature addition and editing.
60 | /// todo: Generic selector creator?
61 | fn feature_type_picker(val: &mut FeatureType, id: usize, ui: &mut Ui) {
62 | ComboBox::from_id_salt(id)
63 | .width(140.)
64 | .selected_text(val.to_string())
65 | .show_ui(ui, |ui| {
66 | for feature_type in [
67 | FeatureType::Generic,
68 | FeatureType::CodingRegion,
69 | FeatureType::Ori,
70 | // FeatureType::RnaPolyBindSite,
71 | FeatureType::RibosomeBindSite,
72 | FeatureType::AntibioticResistance,
73 | FeatureType::LongTerminalRepeat,
74 | FeatureType::Exon,
75 | FeatureType::Transcript,
76 | // todo: Source?
77 | ] {
78 | ui.selectable_value(val, feature_type, feature_type.to_string());
79 | }
80 | });
81 | }
82 |
83 | pub fn feature_table(state: &mut State, ui: &mut Ui) {
84 | feature_add_disp(state, ui);
85 | ui.add_space(ROW_SPACING);
86 |
87 | let mut removed = None;
88 | for (i, feature) in state.generic[state.active].features.iter_mut().enumerate() {
89 | let mut border_width = 0.;
90 | if let Selection::Feature(j) = state.ui.selected_item {
91 | if i == j {
92 | border_width = 1.;
93 | }
94 | }
95 |
96 | Frame::none()
97 | .stroke(Stroke::new(border_width, Color32::LIGHT_RED))
98 | .inner_margin(border_width)
99 | .show(ui, |ui| {
100 | if ui
101 | .heading(RichText::new(&feature.label).color(COLOR_ACTION))
102 | .on_hover_cursor(CursorIcon::PointingHand)
103 | .clicked()
104 | {
105 | state.ui.selected_item = Selection::Feature(i);
106 | }
107 |
108 | ui.horizontal(|ui| {
109 | int_field(&mut feature.range.start, "Start:", ui);
110 | int_field(&mut feature.range.end, "End:", ui);
111 |
112 | ui.label("Label:");
113 | ui.add(
114 | TextEdit::singleline(&mut feature.label).desired_width(LABEL_EDIT_WIDTH),
115 | );
116 |
117 | ui.label("Type:");
118 | feature_type_picker(&mut feature.feature_type, 100 + i, ui);
119 |
120 | ui.label("Dir:");
121 | direction_picker(&mut feature.direction, 300 + i, ui);
122 |
123 | ui.label("Custom color:");
124 | color_picker(
125 | &mut feature.color_override,
126 | feature.feature_type.color(),
127 | ui,
128 | );
129 |
130 | if ui.button("Add note").clicked() {
131 | feature.notes.push((String::new(), String::new()));
132 | }
133 |
134 | // todo: This section repetative with primers.
135 | let mut selected = false;
136 | if let Selection::Feature(sel_i) = state.ui.selected_item {
137 | if sel_i == i {
138 | selected = true;
139 | }
140 | }
141 |
142 | if selected {
143 | if ui
144 | .button(RichText::new("🔘").color(Color32::GREEN))
145 | .clicked()
146 | {
147 | state.ui.selected_item = Selection::None;
148 | }
149 | } else if ui.button("🔘").clicked() {
150 | state.ui.selected_item = Selection::Feature(i);
151 | }
152 |
153 | ui.add_space(COL_SPACING); // Less likely to accidentally delete.
154 |
155 | if ui
156 | .button(RichText::new("Delete 🗑").color(Color32::RED))
157 | .clicked()
158 | {
159 | removed = Some(i);
160 | }
161 | });
162 |
163 | for (key, value) in &mut feature.notes {
164 | ui.horizontal(|ui| {
165 | ui.label("Note:");
166 | ui.add(TextEdit::singleline(key).desired_width(140.));
167 |
168 | ui.label("Value:");
169 | ui.add(TextEdit::singleline(value).desired_width(600.));
170 | });
171 | }
172 | });
173 |
174 | ui.add_space(ROW_SPACING);
175 | }
176 | if let Some(rem_i) = removed {
177 | state.generic[state.active].features.remove(rem_i);
178 | }
179 | }
180 |
181 | pub fn feature_add_disp(state: &mut State, ui: &mut Ui) {
182 | ui.horizontal(|ui| {
183 | if ui.button("➕ Add feature").clicked() {
184 | if state.ui.feature_add.start_posit == 0 {
185 | state.ui.feature_add.start_posit = 1;
186 | }
187 | if state.ui.feature_add.end_posit == 0 {
188 | state.ui.feature_add.end_posit = 1;
189 | }
190 |
191 | if state.ui.feature_add.start_posit > state.ui.feature_add.end_posit {
192 | std::mem::swap(
193 | &mut state.ui.feature_add.start_posit,
194 | &mut state.ui.feature_add.end_posit,
195 | );
196 | }
197 |
198 | state.generic[state.active].features.push(Feature {
199 | range: RangeIncl::new(
200 | state.ui.feature_add.start_posit,
201 | state.ui.feature_add.end_posit,
202 | ),
203 | feature_type: FeatureType::Generic,
204 | direction: FeatureDirection::None,
205 | label: state.ui.feature_add.label.clone(),
206 | color_override: None,
207 | notes: Default::default(),
208 | });
209 | }
210 | });
211 | }
212 |
213 | pub fn features_page(state: &mut State, ui: &mut Ui) {
214 | ScrollArea::vertical().show(ui, |ui| {
215 | feature_table(state, ui);
216 | });
217 | }
218 |
--------------------------------------------------------------------------------
/src/gui/input.rs:
--------------------------------------------------------------------------------
1 | //! Code related to mouse and keyboard input handling.
2 |
3 | use std::{mem, path::PathBuf};
4 |
5 | use eframe::egui::{Event, InputState, Key, PointerButton, Ui};
6 | use na_seq::{Nucleotide, seq_from_str};
7 |
8 | use crate::{
9 | StateUi,
10 | file_io::{
11 | save,
12 | save::{QUICKSAVE_FILE, StateToSave, load_import},
13 | },
14 | gui::{
15 | navigation::{Page, Tab},
16 | set_window_title,
17 | },
18 | state::State,
19 | util::RangeIncl,
20 | };
21 |
22 | /// Handle hotkeys and clicks that affect all pages.
23 | fn handle_global(state: &mut State, ip: &InputState) {
24 | if ip.key_pressed(Key::A) && ip.modifiers.ctrl && !state.ui.text_edit_active {
25 | if !state.get_seq().is_empty() {
26 | state.ui.text_selection = Some(RangeIncl::new(1, state.get_seq().len()))
27 | }
28 | }
29 |
30 | if ip.key_pressed(Key::S) && ip.modifiers.ctrl && !ip.modifiers.shift {
31 | save::save_current_file(state);
32 | }
33 |
34 | if ip.key_pressed(Key::N) && ip.modifiers.ctrl {
35 | state.add_tab();
36 | state.tabs_open.push(Tab {
37 | path: None,
38 | ab1: false,
39 | });
40 | }
41 |
42 | if ip.key_pressed(Key::F) && ip.modifiers.ctrl {
43 | state.ui.highlight_search_input = true;
44 | // Disable the cursor, so we don't insert nucleotides while searching!
45 | state.ui.text_cursor_i = None;
46 | }
47 |
48 | if ip.key_pressed(Key::S) && ip.modifiers.ctrl && ip.modifiers.shift {
49 | state.ui.file_dialogs.save.pick_file();
50 | }
51 |
52 | if ip.key_pressed(Key::O) && ip.modifiers.ctrl {
53 | state.ui.file_dialogs.load.pick_file();
54 | }
55 |
56 | state.ui.cursor_pos = ip.pointer.hover_pos().map(|pos| (pos.x, pos.y));
57 |
58 | if ip.pointer.button_clicked(PointerButton::Primary) {
59 | // todo: Not working for fixing our fast-sel off-by-one bug.
60 | // if let Some(i) = &state.ui.cursor_seq_i {
61 | // state.ui.text_selection = Some(*i..=*i); // 1-based indexing. Second value is a placeholder.
62 | // }
63 |
64 | state.ui.click_pending_handle = true;
65 | }
66 |
67 | if ip.pointer.button_double_clicked(PointerButton::Primary) {
68 | state.ui.dblclick_pending_handle = true;
69 | }
70 | }
71 |
72 | /// Handle sequence selection on the sequence page, as when dragging the mouse.
73 | fn handle_seq_selection(state_ui: &mut StateUi, dragging: bool) {
74 | if dragging {
75 | // A drag has started.
76 | if !state_ui.dragging {
77 | state_ui.dragging = true;
78 | // We are handling in the seq view after setting cursor seq i. Still glitchy.
79 |
80 | if let Some(i) = &state_ui.cursor_seq_i {
81 | state_ui.text_selection = Some(RangeIncl::new(*i, *i)); // The end value is a placeholder.
82 | }
83 | } else {
84 | // The drag is in progress; continually update the selection, for visual feedback.
85 | if let Some(i) = &state_ui.cursor_seq_i {
86 | if let Some(sel_range) = &mut state_ui.text_selection {
87 | sel_range.end = *i;
88 | }
89 | }
90 | }
91 | } else {
92 | // A drag has ended.
93 | if state_ui.dragging {
94 | state_ui.dragging = false;
95 |
96 | // This logic allows for reverse-order selecting, ie dragging the cursor from the end to
97 | // the start of the region. Handle in the UI how to display this correctly while the drag is in process,
98 | // since this only resolves once it's complete.
99 | if let Some(sel_range) = &mut state_ui.text_selection {
100 | if sel_range.start > sel_range.end {
101 | mem::swap(&mut sel_range.start, &mut sel_range.end);
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
108 | /// Handles keyboard and mouse input not associated with a widget.
109 | /// todo: MOve to a separate module if this becomes complex.
110 | pub fn handle_input(state: &mut State, ui: &mut Ui) {
111 | let mut reset_window_title = false; // This setup avoids borrow errors.
112 |
113 | ui.ctx().input(|ip| {
114 | // Check for file drop
115 | if let Some(dropped_files) = ip.raw.dropped_files.first() {
116 | if let Some(path) = &dropped_files.path {
117 | if let Some(loaded) = load_import(path) {
118 | state.load(&loaded);
119 | reset_window_title = true;
120 | }
121 | }
122 | }
123 |
124 | handle_global(state, ip);
125 |
126 | if state.ui.text_edit_active && !reset_window_title {
127 | return;
128 | }
129 |
130 | // This event match is not specific to the seqe page
131 | for event in &ip.events {
132 | match event {
133 | Event::Cut => state.copy_seq(),
134 | Event::Copy => state.copy_seq(),
135 | Event::Paste(pasted_text) => {}
136 | _ => (),
137 | }
138 | }
139 |
140 | if let Page::Sequence = state.ui.page {
141 | // This is a bit awk; borrow errors.
142 | let mut move_cursor: Option = None;
143 | // todo: How can we control the rate?
144 | if state.ui.text_cursor_i.is_some() {
145 | if ip.key_pressed(Key::ArrowLeft) {
146 | move_cursor = Some(-1);
147 | }
148 | if ip.key_pressed(Key::ArrowRight) {
149 | move_cursor = Some(1);
150 | }
151 | if ip.key_pressed(Key::ArrowUp) {
152 | move_cursor = Some(-(state.ui.nt_chars_per_row as i32));
153 | }
154 | if ip.key_pressed(Key::ArrowDown) {
155 | move_cursor = Some(state.ui.nt_chars_per_row as i32);
156 | }
157 | // Escape key: Remove the text cursor.
158 | if ip.key_pressed(Key::Escape) {
159 | move_cursor = None;
160 | state.ui.text_cursor_i = None;
161 | state.ui.text_selection = None;
162 | }
163 | }
164 |
165 | if ip.key_pressed(Key::Escape) {
166 | state.ui.text_selection = None;
167 | }
168 |
169 | // Insert nucleotides A/R.
170 | if let Some(mut i) = state.ui.text_cursor_i {
171 | if !state.ui.seq_edit_lock {
172 | if i > state.get_seq().len() {
173 | i = 0; // todo?? Having an overflow when backspacing near origin.
174 | }
175 |
176 | let i = i + 1; // Insert after this nucleotide; not before.
177 | // Don't allow accidental nt insertion when the user is entering into the search bar.
178 |
179 | // Add NTs.
180 | if ip.key_pressed(Key::A) && !ip.modifiers.ctrl {
181 | state.insert_nucleotides(&[Nucleotide::A], i);
182 | }
183 | if ip.key_pressed(Key::T) {
184 | state.insert_nucleotides(&[Nucleotide::T], i);
185 | }
186 | if ip.key_pressed(Key::C) && !ip.modifiers.ctrl {
187 | state.insert_nucleotides(&[Nucleotide::C], i);
188 | }
189 | if ip.key_pressed(Key::G) {
190 | state.insert_nucleotides(&[Nucleotide::G], i);
191 | }
192 | if ip.key_pressed(Key::Backspace) && i > 1 {
193 | state.remove_nucleotides(RangeIncl::new(i - 2, i - 2));
194 | }
195 | if ip.key_pressed(Key::Delete) {
196 | state.remove_nucleotides(RangeIncl::new(i - 1, i - 1));
197 | }
198 |
199 | // Paste nucleotides
200 | for event in &ip.events {
201 | match event {
202 | Event::Cut => {
203 | // state.remove_nucleotides();
204 | // move_cursor = Some(pasted_text.len() as i32);
205 | }
206 | Event::Copy => {}
207 | Event::Paste(pasted_text) => {
208 | state.insert_nucleotides(&seq_from_str(pasted_text), i);
209 | move_cursor = Some(pasted_text.len() as i32);
210 | }
211 | _ => (),
212 | }
213 | }
214 | }
215 | }
216 |
217 | if !state.ui.seq_edit_lock {
218 | if ip.key_pressed(Key::A) && !ip.modifiers.ctrl {
219 | move_cursor = Some(1);
220 | }
221 | if ip.key_pressed(Key::T) {
222 | move_cursor = Some(1);
223 | }
224 | if ip.key_pressed(Key::C) && !ip.modifiers.ctrl {
225 | move_cursor = Some(1);
226 | }
227 | if ip.key_pressed(Key::G) {
228 | move_cursor = Some(1);
229 | }
230 |
231 | if ip.key_pressed(Key::Backspace) {
232 | move_cursor = Some(-1);
233 | }
234 | }
235 |
236 | if let Some(i) = &mut state.ui.text_cursor_i {
237 | if let Some(amt) = move_cursor {
238 | let val = *i as i32 + amt;
239 | // If val is 0, the cursor is before the first character; this is OK.
240 | // If it's < 0, we will get an overflow when converting to usize.
241 | if val >= 0 {
242 | let new_posit = val as usize;
243 | if new_posit <= state.generic[state.active].seq.len() {
244 | *i = new_posit;
245 | }
246 | }
247 | }
248 | }
249 |
250 | handle_seq_selection(&mut state.ui, ip.pointer.is_decidedly_dragging());
251 | }
252 | });
253 |
254 | if reset_window_title {
255 | set_window_title(&state.tabs_open[state.active], ui);
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/gui/metadata.rs:
--------------------------------------------------------------------------------
1 | //! Contains references, comments, etc about the plasmid.
2 |
3 | use chrono::NaiveDate;
4 | use eframe::egui::{Color32, RichText, ScrollArea, TextEdit, Ui};
5 |
6 | use crate::{
7 | gui::{COL_SPACING, ROW_SPACING},
8 | misc_types::Metadata,
9 | };
10 |
11 | const WIDTH_RATIO: f32 = 0.6;
12 | const ROW_HEIGHT: usize = 1;
13 |
14 | const HEADING_COLOR: Color32 = Color32::from_rgb(40, 180, 255);
15 |
16 | /// A convenience function to create a text edit for Option
17 | fn option_edit(
18 | val: &mut Option,
19 | label: &str,
20 | multi: bool,
21 | width_: Option,
22 | ui: &mut Ui,
23 | ) {
24 | ui.horizontal(|ui| {
25 | // ui.allocate_exact_size(Vec2::new(LABEL_WIDTH, 0.0), egui::Sense::hover()); // Reserve space
26 | ui.label(label);
27 |
28 | let width = match width_ {
29 | Some(w) => w,
30 | None => ui.available_width() * WIDTH_RATIO,
31 | };
32 |
33 | // todo: Way without cloning?
34 | let mut v = val.clone().unwrap_or_default();
35 | // Don't use these margins if there is a narrow window.
36 | let response = if multi {
37 | ui.add(
38 | TextEdit::multiline(&mut v)
39 | .desired_width(width)
40 | .desired_rows(ROW_HEIGHT),
41 | )
42 | } else {
43 | ui.add(TextEdit::singleline(&mut v).desired_width(width))
44 | };
45 |
46 | if response.changed() {
47 | *val = if !v.is_empty() {
48 | Some(v.to_owned())
49 | } else {
50 | None
51 | };
52 | }
53 | });
54 |
55 | ui.add_space(ROW_SPACING / 2.);
56 | }
57 |
58 | pub fn metadata_page(data: &mut Metadata, ui: &mut Ui) {
59 | // todo: YOu need neat grid alignment. How can we make the labels take up constant space?
60 |
61 | // todo: Examine which fields should be single vs multiline, and the order.
62 | ScrollArea::vertical().show(ui, |ui| {
63 | ui.heading(RichText::new("General:").color(HEADING_COLOR));
64 | ui.add_space(ROW_SPACING / 2.);
65 |
66 | ui.horizontal(|ui| {
67 | ui.label("Plasmid name:");
68 | ui.text_edit_singleline(&mut data.plasmid_name);
69 | });
70 | ui.add_space(ROW_SPACING);
71 |
72 | option_edit(&mut data.definition, "Definition:", false, None, ui);
73 |
74 | ui.horizontal(|ui| {
75 | ui.label("Division:");
76 | ui.add(TextEdit::singleline(&mut data.division).desired_width(60.));
77 | ui.add_space(COL_SPACING);
78 |
79 | option_edit(
80 | &mut data.molecule_type,
81 | "Molecule type:",
82 | false,
83 | Some(80.),
84 | ui,
85 | );
86 |
87 | if let Some(date) = data.date {
88 | ui.add_space(COL_SPACING);
89 | // todo: Allow the date to be editable, and not just when present.
90 | let date = NaiveDate::from_ymd(date.0, date.1.into(), date.2.into());
91 | ui.label(format!("Date: {}", date));
92 | }
93 | });
94 | ui.add_space(ROW_SPACING / 2.);
95 |
96 | option_edit(&mut data.accession, "Accession:", true, None, ui);
97 | option_edit(&mut data.version, "Version:", true, None, ui);
98 | option_edit(&mut data.keywords, "Keywords:", true, None, ui);
99 | option_edit(&mut data.source, "Source:", true, None, ui);
100 | option_edit(&mut data.organism, "Organism:", true, None, ui);
101 |
102 | ui.add_space(ROW_SPACING);
103 |
104 | // pub locus: String,
105 | // pub definition: Option,
106 | // pub accession: Option,
107 | // pub version: Option,
108 | // // pub keywords: Vec,
109 | // pub keywords: Option, // todo vec?
110 | // pub source: Option,
111 | // pub organism: Option,
112 |
113 | ui.heading(RichText::new("References:").color(HEADING_COLOR));
114 | ui.add_space(ROW_SPACING / 2.);
115 |
116 | for ref_ in &mut data.references {
117 | ui.horizontal(|ui| {
118 | ui.label("Title:");
119 | ui.add(
120 | TextEdit::multiline(&mut ref_.title)
121 | .desired_width(ui.available_width() * WIDTH_RATIO)
122 | .desired_rows(ROW_HEIGHT),
123 | );
124 | });
125 | ui.add_space(ROW_SPACING / 2.);
126 |
127 | ui.horizontal(|ui| {
128 | ui.label("Description:");
129 | ui.add(
130 | TextEdit::multiline(&mut ref_.description)
131 | .desired_width(ui.available_width() * WIDTH_RATIO)
132 | .desired_rows(ROW_HEIGHT),
133 | );
134 | });
135 | ui.add_space(ROW_SPACING / 2.);
136 |
137 | option_edit(&mut ref_.authors, "Authors:", true, None, ui);
138 | option_edit(&mut ref_.consortium, "Consortium:", true, None, ui);
139 |
140 | option_edit(&mut ref_.journal, "Journal:", true, None, ui);
141 | option_edit(&mut ref_.pubmed, "Pubmed:", true, None, ui);
142 | option_edit(&mut ref_.remark, "Remarks:", true, None, ui);
143 |
144 | ui.add_space(ROW_SPACING);
145 | }
146 |
147 | // egui::Shape::hline(2, 2., Stroke::new(2., Color32::WHITE));
148 |
149 | ui.heading(RichText::new("Comments:").color(HEADING_COLOR));
150 | ui.add_space(ROW_SPACING);
151 | if ui.button("➕ Add").clicked() {
152 | data.comments.push(String::new());
153 | }
154 |
155 | for comment in &mut data.comments {
156 | let response = ui.add(
157 | TextEdit::multiline(comment)
158 | .desired_width(ui.available_width() * WIDTH_RATIO)
159 | .desired_rows(ROW_HEIGHT),
160 | );
161 | // if response.changed() {
162 | // }
163 |
164 | ui.add_space(ROW_SPACING / 2.);
165 | }
166 | });
167 | }
168 |
--------------------------------------------------------------------------------
/src/gui/navigation.rs:
--------------------------------------------------------------------------------
1 | //! This module contains code related to navigation buttons.
2 |
3 | use std::{fmt::Display, path::PathBuf};
4 |
5 | use bincode::{Decode, Encode};
6 | use eframe::egui::{Color32, RichText, Ui};
7 | use na_seq::seq_to_str_lower;
8 |
9 | use crate::{
10 | gui::{COL_SPACING, ROW_SPACING, select_color_text, set_window_title},
11 | state::State,
12 | };
13 |
14 | pub const NAV_BUTTON_COLOR: Color32 = Color32::from_rgb(0, 0, 110);
15 | pub const TAB_BUTTON_COLOR: Color32 = Color32::from_rgb(40, 80, 110);
16 | pub const DEFAULT_TAB_NAME: &str = "New plasmid";
17 |
18 | // When abbreviating a path, show no more than this many characters.
19 | const PATH_ABBREV_MAX_LEN: usize = 16;
20 |
21 | // todo: Alt name: Path loaded
22 | #[derive(Encode, Decode, Clone, Default, Debug)]
23 | pub struct Tab {
24 | pub path: Option,
25 | pub ab1: bool, // todo: Enum if you add a third category.
26 | }
27 |
28 | /// Used in several GUI components to get data from open tabs.
29 | /// Note: For name, we currently default to file name (with extension), then
30 | /// plasmid name, then a default. See if you want to default to plasmid name.
31 | ///
32 | /// Returns the name, and the tab index.
33 | pub fn get_tab_names(
34 | tabs: &[Tab],
35 | plasmid_names: &[&str],
36 | abbrev_name: bool,
37 | ) -> Vec<(String, usize)> {
38 | let mut result = Vec::new();
39 |
40 | for (i, p) in tabs.iter().enumerate() {
41 | let name = name_from_path(&p.path, plasmid_names[i], abbrev_name);
42 | result.push((name, i));
43 | }
44 |
45 | result
46 | }
47 |
48 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
49 | pub enum Page {
50 | /// Primer design and QC, including for cloning
51 | Sequence,
52 | /// A circular "graphical map" of the plasmid
53 | Map,
54 | Features,
55 | Primers,
56 | Proteins,
57 | /// Determine optimal PCR parameters
58 | Pcr,
59 | Alignment,
60 | Portions,
61 | Metadata,
62 | Ligation,
63 | /// A simplified cloning process, for both PCR and RE-based cloning.
64 | Cloning,
65 | /// i.e. Sanger sequencing data. This page is fundamentally different from the others;
66 | /// it is only for .ab1 files, and is selected automatically, vice from the menu.
67 | Ab1,
68 | }
69 |
70 | impl Default for Page {
71 | fn default() -> Self {
72 | Self::Sequence
73 | }
74 | }
75 |
76 | impl Display for Page {
77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 | let str = match self {
79 | Self::Sequence => "Sequence",
80 | Self::Map => "Map",
81 | Self::Features => "Features",
82 | Self::Primers => "Primers",
83 | Self::Proteins => "Proteins",
84 | Self::Pcr => "PCR",
85 | Self::Alignment => "Align",
86 | Self::Portions => "Mixing",
87 | Self::Metadata => "Data",
88 | Self::Ligation => "Digest",
89 | Self::Cloning => "Clone",
90 | Self::Ab1 => "AB1",
91 | }
92 | .to_owned();
93 | write!(f, "{}", str)
94 | }
95 | }
96 |
97 | /// Selects which tab (ie file) is active
98 | pub fn tab_selector(state: &mut State, ui: &mut Ui) {
99 | // Note: This assumes generic, paths_loaded etc are always the same length. (Which they *should* be.)
100 |
101 | let mut tab_removed = None;
102 |
103 | ui.horizontal(|ui| {
104 | if ui
105 | .button("New")
106 | .on_hover_text("Create and open a new file. (Ctrl + N)")
107 | .clicked()
108 | {
109 | state.add_tab();
110 | state.tabs_open.push(Default::default());
111 | }
112 | ui.add_space(COL_SPACING);
113 |
114 | let plasmid_names: &Vec<_> = &state
115 | .generic
116 | .iter()
117 | .map(|v| v.metadata.plasmid_name.as_str())
118 | .collect();
119 |
120 | for (name, i) in get_tab_names(&state.tabs_open, plasmid_names, false) {
121 | // todo: DRY with page selectors.
122 |
123 | let button = ui.button(
124 | select_color_text(&name, i == state.active).background_color(TAB_BUTTON_COLOR),
125 | );
126 |
127 | if button.clicked() {
128 | state.active = i;
129 | set_window_title(&state.tabs_open[i], ui);
130 |
131 | // todo: Apt state sync fn for this?
132 | state.ui.seq_input = seq_to_str_lower(state.get_seq()); // todo: Move seq_input to an indexed vector?
133 | // todo: Cache these instead?
134 | // state.sync_seq_related(None);
135 | }
136 |
137 | if button.middle_clicked() {
138 | tab_removed = Some(i);
139 | }
140 |
141 | ui.add_space(COL_SPACING / 2.);
142 | }
143 |
144 | // todo: Right-align?
145 | ui.add_space(2. * ROW_SPACING);
146 |
147 | if ui
148 | .button(
149 | RichText::new("Close active tab")
150 | .color(Color32::WHITE)
151 | .background_color(Color32::DARK_RED),
152 | )
153 | .on_hover_text("Shortcut: Middle click the tab to close it.")
154 | .clicked()
155 | {
156 | tab_removed = Some(state.active)
157 | };
158 | });
159 |
160 | if let Some(i) = tab_removed {
161 | state.remove_tab(i);
162 | }
163 | }
164 |
165 | pub fn page_selector(state: &mut State, ui: &mut Ui) {
166 | ui.horizontal(|ui| {
167 | page_button(&mut state.ui.page, Page::Sequence, ui, true);
168 | page_button(&mut state.ui.page, Page::Map, ui, true);
169 | page_button(&mut state.ui.page, Page::Features, ui, true);
170 | page_button(&mut state.ui.page, Page::Primers, ui, true);
171 | page_button(&mut state.ui.page, Page::Proteins, ui, true);
172 | page_button(&mut state.ui.page, Page::Cloning, ui, true);
173 | page_button(&mut state.ui.page, Page::Pcr, ui, true);
174 | page_button(&mut state.ui.page, Page::Alignment, ui, true);
175 | page_button(&mut state.ui.page, Page::Ligation, ui, true);
176 | page_button(&mut state.ui.page, Page::Metadata, ui, true);
177 | page_button(&mut state.ui.page, Page::Portions, ui, true);
178 | });
179 | }
180 |
181 | pub fn page_button(page_state: &mut T, page: T, ui: &mut Ui, space: bool) {
182 | if ui
183 | .button(
184 | select_color_text(&page.to_string(), *page_state == page)
185 | .background_color(NAV_BUTTON_COLOR),
186 | )
187 | .clicked()
188 | {
189 | *page_state = page;
190 | }
191 |
192 | if space {
193 | ui.add_space(COL_SPACING / 2.);
194 | }
195 | }
196 |
197 | /// This is used for selecting what is displayed in the sequence view, ie view or edit.
198 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
199 | pub enum PageSeq {
200 | EditRaw,
201 | // EditSlic,
202 | View,
203 | }
204 |
205 | impl Default for PageSeq {
206 | fn default() -> Self {
207 | Self::View
208 | }
209 | }
210 |
211 | impl Display for PageSeq {
212 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 | let str = match self {
214 | Self::EditRaw => "Edit raw",
215 | // Self::EditSlic => "SLIC/FC cloning",
216 | Self::View => "Sequence",
217 | }
218 | .to_owned();
219 | write!(f, "{}", str)
220 | }
221 | }
222 |
223 | pub fn page_seq_selector(state: &mut State, ui: &mut Ui) {
224 | ui.horizontal(|ui| {
225 | page_button(&mut state.ui.page_seq, PageSeq::EditRaw, ui, true);
226 | page_button(&mut state.ui.page_seq, PageSeq::View, ui, true);
227 | });
228 | }
229 |
230 | /// This is used for selecting what is displayed above the sequence view, ie various tabular editors.
231 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
232 | pub enum PageSeqTop {
233 | Primers,
234 | Features,
235 | None,
236 | }
237 |
238 | impl Default for PageSeqTop {
239 | fn default() -> Self {
240 | Self::None
241 | }
242 | }
243 |
244 | impl Display for PageSeqTop {
245 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 | let str = match self {
247 | Self::Primers => "Primers",
248 | Self::Features => "Features",
249 | Self::None => "None",
250 | }
251 | .to_owned();
252 | write!(f, "{}", str)
253 | }
254 | }
255 |
256 | pub fn page_seq_top_selector(state: &mut State, ui: &mut Ui) {
257 | ui.horizontal(|ui| {
258 | ui.label("Display above sequence:");
259 |
260 | page_button(&mut state.ui.page_seq_top, PageSeqTop::None, ui, true);
261 | page_button(&mut state.ui.page_seq_top, PageSeqTop::Features, ui, true);
262 | page_button(&mut state.ui.page_seq_top, PageSeqTop::Primers, ui, true);
263 | });
264 | }
265 |
266 | /// A short, descriptive name for a given opened tab.
267 | pub fn name_from_path(path: &Option, plasmid_name: &str, abbrev_name: bool) -> String {
268 | let mut name = match path {
269 | Some(path) => path
270 | .file_name()
271 | .and_then(|name| name.to_str())
272 | .map(|name_str| name_str.to_string())
273 | .unwrap(),
274 | None => {
275 | if !plasmid_name.is_empty() {
276 | plasmid_name.to_owned()
277 | } else {
278 | DEFAULT_TAB_NAME.to_owned()
279 | }
280 | }
281 | };
282 |
283 | if abbrev_name && name.len() > PATH_ABBREV_MAX_LEN {
284 | name = format!("{}...", &name[..PATH_ABBREV_MAX_LEN].to_string())
285 | }
286 |
287 | name
288 | }
289 |
--------------------------------------------------------------------------------
/src/gui/pcr.rs:
--------------------------------------------------------------------------------
1 | use std::str::FromStr;
2 |
3 | use eframe::egui::{Color32, ComboBox, Grid, RichText, TextEdit, Ui, Vec2};
4 |
5 | use crate::{
6 | gui::{
7 | COL_SPACING, ROW_SPACING, lin_maps,
8 | theme::{COLOR_ACTION, COLOR_INFO},
9 | },
10 | pcr::{PcrUi, PolymeraseType, TempTime, make_amplicon_tab},
11 | primer::{Primer, PrimerDirection, TM_TARGET},
12 | state::State,
13 | util::RangeIncl,
14 | };
15 |
16 | fn temp_time_disp(tt: &TempTime, label: &str, ui: &mut Ui) {
17 | ui.label(&format!("{label}:"));
18 | ui.label(RichText::new(format!("{:.0}°C", tt.temp)).color(COLOR_INFO));
19 | ui.label(RichText::new(format!("{}s", tt.time)).color(COLOR_INFO));
20 |
21 | ui.end_row();
22 | }
23 |
24 | fn numerical_field(label: &str, val: &mut T, default: T, ui: &mut Ui) -> bool
25 | where
26 | T: ToString + FromStr + Copy,
27 | ::Err: std::fmt::Debug,
28 | {
29 | let mut changed = false;
30 |
31 | ui.label(&format!("{}: ", label));
32 | let mut entry = val.to_string();
33 | if ui
34 | .add(TextEdit::singleline(&mut entry).desired_width(30.))
35 | .changed()
36 | {
37 | *val = entry.parse().unwrap_or(default);
38 | changed = true;
39 | }
40 |
41 | changed
42 | }
43 |
44 | fn primer_dropdown(
45 | val: &mut usize,
46 | primers: &[Primer],
47 | direction: Option,
48 | id: usize,
49 | ui: &mut Ui,
50 | ) {
51 | // Reset primer selected if an invalid one is set.
52 | if *val > primers.len() {
53 | *val = 0;
54 | }
55 |
56 | let primer = &primers[*val];
57 |
58 | ComboBox::from_id_salt(id)
59 | .width(80.)
60 | .selected_text(&primer.name)
61 | .show_ui(ui, |ui| {
62 | for (i, primer) in primers.iter().enumerate() {
63 | if let Some(dir) = direction {
64 | let mut dir_match = false;
65 | for match_ in &primer.volatile.matches {
66 | if match_.direction == dir {
67 | dir_match = true;
68 | }
69 | }
70 | if !dir_match {
71 | continue;
72 | }
73 | }
74 |
75 | ui.selectable_value(val, i, &primer.name);
76 | }
77 | });
78 | }
79 |
80 | fn pcr_sim(state: &mut State, ui: &mut Ui) {
81 | let num_primers = state.generic[state.active].primers.len();
82 |
83 | if state.ui.pcr.primer_fwd >= num_primers || state.ui.pcr.primer_rev >= num_primers {
84 | state.ui.pcr.primer_fwd = 0;
85 | state.ui.pcr.primer_rev = 0;
86 | }
87 |
88 | ui.heading("PCR product generation");
89 |
90 | lin_maps::seq_lin_disp(
91 | &state.generic[state.active],
92 | false,
93 | state.ui.selected_item,
94 | &Vec::new(),
95 | None,
96 | &state.ui,
97 | &state.volatile[state.active].restriction_enzyme_matches,
98 | &state.restriction_enzyme_lib,
99 | ui,
100 | );
101 | ui.add_space(ROW_SPACING / 2.);
102 |
103 | if num_primers >= 2 {
104 | ui.horizontal(|ui| {
105 | ui.label("Fwd:");
106 | primer_dropdown(
107 | &mut state.ui.pcr.primer_fwd,
108 | &state.generic[state.active].primers,
109 | Some(PrimerDirection::Forward),
110 | 2,
111 | ui,
112 | );
113 |
114 | ui.add_space(COL_SPACING);
115 |
116 | ui.label("Rev:");
117 | primer_dropdown(
118 | &mut state.ui.pcr.primer_rev,
119 | &state.generic[state.active].primers,
120 | Some(PrimerDirection::Reverse),
121 | 3,
122 | ui,
123 | );
124 |
125 | ui.add_space(COL_SPACING);
126 |
127 | // let fwd_primer = &state.generic[state.active].primers[state.ui.pcr.primer_fwd];
128 | // let rev_primer = &state.generic[state.active].primers[state.ui.pcr.primer_rev];
129 |
130 | // todo: Yikes on this syntax.
131 | if state.ui.pcr.primer_fwd == state.ui.pcr.primer_rev {
132 | ui.label("Select two different primers");
133 | } else if state.generic[state.active].primers[state.ui.pcr.primer_fwd]
134 | .volatile
135 | .matches
136 | .len()
137 | == 1
138 | && state.generic[state.active].primers[state.ui.pcr.primer_rev]
139 | .volatile
140 | .matches
141 | .len()
142 | == 1
143 | {
144 | if ui
145 | .button(RichText::new("Simulate PCR").color(COLOR_ACTION))
146 | .clicked()
147 | {
148 | let fwd_primer =
149 | state.generic[state.active].primers[state.ui.pcr.primer_fwd].clone();
150 | let rev_primer =
151 | state.generic[state.active].primers[state.ui.pcr.primer_rev].clone();
152 |
153 | // todo: Yikes.
154 | let range_fwd = fwd_primer.volatile.matches[0].range;
155 | let range_rev = rev_primer.volatile.matches[0].range;
156 |
157 | let range_combined = RangeIncl::new(range_fwd.start, range_rev.end);
158 |
159 | let product_seq = if range_combined.start > range_combined.end {
160 | range_combined
161 | .index_seq_wrap(&state.generic[state.active].seq)
162 | .unwrap()
163 | // todo unwrap is dicey.
164 | } else {
165 | range_combined
166 | .index_seq(&state.generic[state.active].seq)
167 | .unwrap()
168 | .to_vec() // todo unwrap is dicey.
169 | };
170 |
171 | make_amplicon_tab(state, product_seq, range_combined, fwd_primer, rev_primer);
172 | }
173 | } else {
174 | ui.label("There must be exactly one match for each primer");
175 | }
176 | });
177 | } else {
178 | ui.label("(Add at least 2 primers to generate a PCR product)");
179 | }
180 | }
181 |
182 | pub fn pcr_page(state: &mut State, ui: &mut Ui) {
183 | pcr_sim(state, ui);
184 | ui.add_space(ROW_SPACING * 2.);
185 |
186 | ui.horizontal(|ui| {
187 | ui.heading("PCR parameters");
188 | if !state.generic[state.active].primers.is_empty() {
189 | ui.add_space(COL_SPACING);
190 |
191 | if ui.button("Load from primer: ").clicked() {
192 | let primer = &state.generic[state.active].primers[state.ui.pcr.primer_selected]; // todo: Overflow check?
193 |
194 | if let Some(metrics) = &primer.volatile.metrics {
195 | state.ui.pcr = PcrUi {
196 | primer_tm: metrics.melting_temp,
197 | product_len: state.get_seq().len(),
198 | primer_selected: state.ui.pcr.primer_selected,
199 | ..Default::default()
200 | };
201 | }
202 | state.sync_pcr();
203 | }
204 |
205 | primer_dropdown(
206 | &mut state.ui.pcr.primer_selected,
207 | &state.generic[state.active].primers,
208 | None,
209 | 1,
210 | ui,
211 | );
212 | }
213 | });
214 |
215 | // pub primer_tm: f32,
216 | // pub product_len: usize,
217 | // pub polymerase_type: PolymeraseType,
218 | // pub num_cycles: u16,
219 |
220 | ui.horizontal(|ui| {
221 | // todo: Allow TM decimals?
222 | // Not using our helper here due to int coercing.
223 | ui.label("Primer TM");
224 | let mut entry = format!("{:.0}", state.ui.pcr.primer_tm);
225 | let response = ui.add(TextEdit::singleline(&mut entry).desired_width(20.));
226 | if response.changed() {
227 | state.ui.pcr.primer_tm = entry.parse().unwrap_or(TM_TARGET);
228 | state.sync_pcr();
229 | }
230 |
231 | // if numerical_field("Primer TM", &mut state.ui.pcr.primer_tm, TM_TARGET, ui) ||
232 | if numerical_field(
233 | "Product size (bp)",
234 | &mut state.ui.pcr.product_len,
235 | 1_000,
236 | ui,
237 | ) || numerical_field("# cycles", &mut state.ui.pcr.num_cycles, 30, ui)
238 | {
239 | state.sync_pcr();
240 | }
241 |
242 | ui.label("Polymerase:");
243 | let prev_poly = state.ui.pcr.polymerase_type;
244 | ComboBox::from_id_salt(10)
245 | .width(80.)
246 | .selected_text(state.ui.pcr.polymerase_type.to_str())
247 | .show_ui(ui, |ui| {
248 | ui.selectable_value(
249 | &mut state.ui.pcr.polymerase_type,
250 | PolymeraseType::NormalFidelity,
251 | PolymeraseType::NormalFidelity.to_str(),
252 | );
253 | ui.selectable_value(
254 | &mut state.ui.pcr.polymerase_type,
255 | PolymeraseType::HighFidelity,
256 | PolymeraseType::HighFidelity.to_str(),
257 | );
258 | });
259 |
260 | if state.ui.pcr.polymerase_type != prev_poly {
261 | state.sync_pcr();
262 | }
263 | });
264 |
265 | ui.add_space(ROW_SPACING);
266 |
267 | Grid::new(0).spacing(Vec2::new(60., 0.)).show(ui, |ui| {
268 | temp_time_disp(&state.pcr.initial_denaturation, "Initial denaturation", ui);
269 | temp_time_disp(&state.pcr.denaturation, "Denaturation", ui);
270 | temp_time_disp(&state.pcr.annealing, "Annealing", ui);
271 | temp_time_disp(&state.pcr.extension, "Extension", ui);
272 | temp_time_disp(&state.pcr.final_extension, "Final extension", ui);
273 |
274 | ui.label("Number of cycles:".to_string());
275 | ui.label(format!("{}", state.pcr.num_cycles));
276 |
277 | ui.end_row();
278 | });
279 | }
280 |
--------------------------------------------------------------------------------
/src/gui/portions.rs:
--------------------------------------------------------------------------------
1 | //! UI page for mixing portions. (growth media, stock solutions etc)
2 |
3 | use eframe::{
4 | egui,
5 | egui::{Color32, ComboBox, RichText, TextEdit, Ui},
6 | };
7 |
8 | use crate::{
9 | gui::{
10 | COL_SPACING, ROW_SPACING,
11 | theme::{COLOR_ACTION, COLOR_INFO},
12 | },
13 | portions::{
14 | MediaPrepInput, PlateSize, PortionsState, Reagent, ReagentPrep, ReagentType, Solution,
15 | media_prep,
16 | },
17 | };
18 | // todo: Make a non-gui portions module once this becomes unweildy.
19 |
20 | // todo: Store solutions. Save to file, and mix solutions from other solutions.
21 |
22 | const DEFAULT_REAGENT_MOLARITY: f32 = 1.;
23 | const DEFAULT_TOTAL_VOLUME: f32 = 1.; // L
24 |
25 | fn solutions_disp(portions: &mut PortionsState, ui: &mut Ui) {
26 | let mut sol_removed = None;
27 | for (i, solution) in portions.solutions.iter_mut().enumerate() {
28 | ui.horizontal(|ui| {
29 | // ui.heading(RichText::new(&solution.name).color(Color32::LIGHT_BLUE));
30 | ui.add(TextEdit::singleline(&mut solution.name).desired_width(200.));
31 |
32 | ui.label("Total volume (mL):");
33 |
34 | // todo: Float field, or can we do ml?
35 | let mut val_ml = ((solution.total_volume * 1_000.) as u32).to_string();
36 | let response = ui.add(TextEdit::singleline(&mut val_ml).desired_width(40.));
37 |
38 | if response.changed() {
39 | let val_int: u32 = val_ml.parse().unwrap_or_default();
40 | solution.total_volume = val_int as f32 / 1_000.;
41 | solution.calc_amounts();
42 | }
43 |
44 | if ui
45 | .button(RichText::new("➕ Add reagent").color(COLOR_ACTION))
46 | .clicked()
47 | {
48 | solution.reagents.push(Reagent::default());
49 | }
50 |
51 | if ui
52 | .button(RichText::new("Delete solution 🗑").color(Color32::RED))
53 | .clicked()
54 | {
55 | sol_removed = Some(i);
56 | }
57 | });
58 |
59 | ui.add_space(ROW_SPACING / 2.);
60 |
61 | let mut reagent_removed = None;
62 |
63 | for (j, reagent) in solution.reagents.iter_mut().enumerate() {
64 | ui.horizontal(|ui| {
65 | let type_prev = reagent.type_;
66 | ComboBox::from_id_salt(100 + i * 100 + j)
67 | .width(140.)
68 | .selected_text(reagent.type_.to_string())
69 | .show_ui(ui, |ui| {
70 | for type_ in [
71 | ReagentType::Custom(0.),
72 | ReagentType::SodiumChloride,
73 | ReagentType::TrisHcl,
74 | ReagentType::Iptg,
75 | ReagentType::SodiumPhosphateMonobasic,
76 | ReagentType::SodiumPhosphateDibasic,
77 | ReagentType::SodiumPhosphateDibasicHeptahydrate,
78 | ReagentType::PotassiumPhosphateMonobasic,
79 | ReagentType::PotassiumPhosphateDibasic,
80 | ReagentType::Imidazole,
81 | ReagentType::Lysozyme,
82 | ReagentType::Mes,
83 | ReagentType::Bes,
84 | ReagentType::Tes,
85 | ReagentType::CitricAcid,
86 | ReagentType::Edta,
87 | ReagentType::HydrochloricAcid,
88 | ReagentType::SodiumHydroxide,
89 | ReagentType::BromophenolBlue,
90 | ReagentType::Dtt,
91 | ReagentType::MagnesiumChloride,
92 | ReagentType::Sds,
93 | ReagentType::Glycine,
94 | ReagentType::Tris,
95 | ] {
96 | ui.selectable_value(&mut reagent.type_, type_, type_.to_string());
97 | }
98 |
99 | // ui.selectable_value(val, Reagent::Custom(_), Reagent::Custom(_).to_string());
100 | });
101 |
102 | if let ReagentType::Custom(weight) = &mut reagent.type_ {
103 | let mut val = format!("{:.2}", weight);
104 | if ui
105 | .add(TextEdit::singleline(&mut val).desired_width(40.))
106 | .changed()
107 | {
108 | // let molarity_int: u32 = val.parse().unwrap_or_default();
109 | *weight = val.parse().unwrap_or_default();
110 | // *v = molarity_int as f32 / 1_000.;
111 | reagent.calc_amount(solution.total_volume);
112 | }
113 | ui.label("g/mol");
114 | } else {
115 | ui.add_sized(
116 | [80.0, 20.0],
117 | egui::Label::new(format!("{:.2} g/mol", reagent.type_.weight())),
118 | );
119 | }
120 |
121 | let prep_prev = reagent.prep;
122 | ComboBox::from_id_salt(2000 + i * 100 + j)
123 | .width(80.)
124 | .selected_text(reagent.prep.to_string())
125 | .show_ui(ui, |ui| {
126 | for prep in [
127 | ReagentPrep::Mass,
128 | ReagentPrep::Volume(DEFAULT_REAGENT_MOLARITY),
129 | ] {
130 | ui.selectable_value(&mut reagent.prep, prep, prep.to_string());
131 | }
132 | });
133 |
134 | // todo: This effect on water volume added?
135 | if let ReagentPrep::Volume(v) = &mut reagent.prep {
136 | ui.label("reagant Molarity (mM):");
137 | let mut val = ((*v * 1_000.) as u32).to_string();
138 | if ui
139 | .add(TextEdit::singleline(&mut val).desired_width(40.))
140 | .changed()
141 | {
142 | let molarity_int: u32 = val.parse().unwrap_or_default();
143 | *v = molarity_int as f32 / 1_000.;
144 | reagent.calc_amount(solution.total_volume);
145 | }
146 | }
147 |
148 | if type_prev != reagent.type_ || prep_prev != reagent.prep {
149 | reagent.calc_amount(solution.total_volume);
150 | }
151 |
152 | ui.add_space(COL_SPACING / 2.);
153 | ui.label("Molarity (mM):");
154 |
155 | // Convert to a mM integer.
156 | let mut val = ((reagent.molarity * 1_000.) as u32).to_string();
157 | if ui
158 | .add(TextEdit::singleline(&mut val).desired_width(40.))
159 | .changed()
160 | {
161 | let molarity_int: u32 = val.parse().unwrap_or_default();
162 | reagent.molarity = molarity_int as f32 / 1_000.;
163 | reagent.calc_amount(solution.total_volume);
164 | }
165 |
166 | ui.add_space(COL_SPACING / 2.);
167 |
168 | let result_label = if let ReagentPrep::Volume(_) = reagent.prep {
169 | "Volume"
170 | } else {
171 | "Mass"
172 | };
173 |
174 | ui.label(result_label);
175 |
176 | ui.add_sized(
177 | [80.0, 20.0],
178 | egui::Label::new(
179 | RichText::new(reagent.amount_calc.to_string()).color(COLOR_INFO),
180 | ),
181 | );
182 |
183 | ui.add_space(COL_SPACING);
184 | if ui.button(RichText::new("🗑").color(Color32::RED)).clicked() {
185 | reagent_removed = Some(j);
186 | }
187 | });
188 | }
189 |
190 | if let Some(rem_i) = reagent_removed {
191 | solution.reagents.remove(rem_i);
192 | }
193 |
194 | ui.add_space(ROW_SPACING * 2.);
195 | }
196 |
197 | if let Some(rem_i) = sol_removed {
198 | portions.solutions.remove(rem_i);
199 | }
200 | }
201 |
202 | fn media_disp(portions: &mut PortionsState, ui: &mut Ui) {
203 | let type_prev = portions.media_input.clone();
204 | let mut run_calc = false;
205 |
206 | ui.horizontal(|ui| {
207 | ComboBox::from_id_salt(3_000)
208 | .width(110.)
209 | .selected_text(portions.media_input.to_string())
210 | .show_ui(ui, |ui| {
211 | for type_ in [
212 | MediaPrepInput::Plates((PlateSize::D90, 6)),
213 | MediaPrepInput::Liquid(0.),
214 | ] {
215 | ui.selectable_value(
216 | &mut portions.media_input,
217 | type_.clone(),
218 | type_.to_string(),
219 | );
220 | }
221 | });
222 |
223 | ui.add_space(COL_SPACING);
224 |
225 | if portions.media_input != type_prev {
226 | run_calc = true;
227 | }
228 |
229 | match &mut portions.media_input {
230 | MediaPrepInput::Plates((plate_size, num)) => {
231 | ui.label("Plate diameter:");
232 |
233 | let dia_prev = *plate_size;
234 | ComboBox::from_id_salt(3_001)
235 | .width(70.)
236 | .selected_text(plate_size.to_string())
237 | .show_ui(ui, |ui| {
238 | for size in [
239 | PlateSize::D60,
240 | PlateSize::D90,
241 | PlateSize::D100,
242 | PlateSize::D150,
243 | ] {
244 | ui.selectable_value(plate_size, size, size.to_string());
245 | }
246 | });
247 |
248 | if *plate_size != dia_prev {
249 | run_calc = true;
250 | }
251 |
252 | ui.label("Num plates:");
253 |
254 | let mut val = num.to_string();
255 | if ui
256 | .add(TextEdit::singleline(&mut val).desired_width(40.))
257 | .changed()
258 | {
259 | *num = val.parse().unwrap_or_default();
260 | run_calc = true;
261 | }
262 | }
263 | MediaPrepInput::Liquid(volume) => {
264 | ui.label("Volume (mL):");
265 |
266 | let mut val_ml = (*volume * 1_000.).to_string();
267 | if ui
268 | .add(TextEdit::singleline(&mut val_ml).desired_width(50.))
269 | .changed()
270 | {
271 | let val_int: u32 = val_ml.parse().unwrap_or_default();
272 | *volume = val_int as f32 / 1_000.;
273 | run_calc = true;
274 | }
275 | }
276 | }
277 | });
278 |
279 | ui.add_space(ROW_SPACING);
280 |
281 | // todo: Adjust units etc A/R for higherh values.
282 | let result = &portions.media_result;
283 | ui.horizontal(|ui| {
284 | ui.label("Water: ");
285 | ui.label(format!("{:.1} mL", result.water * 1_000.));
286 | ui.add_space(COL_SPACING);
287 |
288 | ui.label("LB: ");
289 | ui.label(format!("{:.2} g", result.food));
290 | ui.add_space(COL_SPACING);
291 |
292 | if result.agar > 0. {
293 | ui.label("Agar: ");
294 | ui.label(format!("{:.2} g", result.agar));
295 | ui.add_space(COL_SPACING);
296 | }
297 |
298 | ui.label("Antibiotic (1000×): ");
299 | ui.label(format!("{:.1} μL", result.antibiotic * 1_000.));
300 | });
301 |
302 | if run_calc {
303 | portions.media_result = media_prep(&portions.media_input);
304 | }
305 | }
306 |
307 | pub fn portions_page(portions: &mut PortionsState, ui: &mut Ui) {
308 | ui.add_space(ROW_SPACING / 2.);
309 |
310 | ui.horizontal(|ui| {
311 | ui.heading("Mixing portions");
312 | ui.add_space(COL_SPACING);
313 |
314 | if ui
315 | .button(RichText::new("➕ Add solution").color(COLOR_ACTION))
316 | .clicked()
317 | {
318 | portions.solutions.push(Solution {
319 | total_volume: DEFAULT_TOTAL_VOLUME,
320 | reagents: vec![Reagent::default()],
321 | ..Default::default()
322 | });
323 | }
324 | });
325 |
326 | ui.add_space(ROW_SPACING);
327 |
328 | solutions_disp(portions, ui);
329 | ui.add_space(ROW_SPACING);
330 |
331 | ui.heading("Growth media");
332 | media_disp(portions, ui);
333 | }
334 |
--------------------------------------------------------------------------------
/src/gui/protein.rs:
--------------------------------------------------------------------------------
1 | use bio_apis::rcsb::{self, PdbData};
2 | use eframe::{
3 | egui::{
4 | Align2, Color32, FontFamily, FontId, Frame, Pos2, Rect, RichText, ScrollArea, Sense, Shape,
5 | Stroke, Ui, pos2, vec2,
6 | },
7 | emath::RectTransform,
8 | epaint::PathShape,
9 | };
10 | use na_seq::{AaIdent, AminoAcid};
11 |
12 | use crate::{
13 | gui::{
14 | BACKGROUND_COLOR, COL_SPACING, ROW_SPACING,
15 | circle::TICK_COLOR,
16 | theme::{COLOR_ACTION, COLOR_INFO},
17 | },
18 | misc_types::FeatureType,
19 | state::State,
20 | };
21 |
22 | const COLOR_PROT_SEQ: Color32 = Color32::from_rgb(255, 100, 200);
23 | const COLOR_PRE_POST_CODING_SEQ: Color32 = Color32::from_rgb(100, 255, 200);
24 | const FONT_SIZE_SEQ: f32 = 14.;
25 |
26 | const CHART_HEIGHT: f32 = 200.;
27 | const CHART_LINE_WIDTH: f32 = 2.;
28 | const CHART_LINE_COLOR: Color32 = Color32::from_rgb(255, 100, 100);
29 |
30 | const NUM_X_TICKS: usize = 12;
31 |
32 | // todo: Color-code AAs, start/stop codons etc.
33 |
34 | // todo: Eval how cacheing and state is handled.
35 |
36 | /// Convert an AA sequence to an ident string.
37 | fn make_aa_text(seq: &[AminoAcid], aa_ident_disp: AaIdent) -> String {
38 | let mut result = String::new();
39 | for aa in seq {
40 | let aa_str = format!("{} ", aa.to_str(aa_ident_disp));
41 | result.push_str(&aa_str);
42 | }
43 |
44 | result
45 | }
46 |
47 | /// Plot a hydrophobicity line plot.
48 | fn hydrophobicity_chart(data: &Vec<(usize, f32)>, ui: &mut Ui) {
49 | let stroke = Stroke::new(CHART_LINE_WIDTH, CHART_LINE_COLOR);
50 |
51 | Frame::canvas(ui.style())
52 | .fill(BACKGROUND_COLOR)
53 | .show(ui, |ui| {
54 | let width = ui.available_width();
55 | let (response, _painter) = {
56 | let desired_size = vec2(width, CHART_HEIGHT);
57 | // ui.allocate_painter(desired_size, Sense::click())
58 | ui.allocate_painter(desired_size, Sense::click_and_drag())
59 | };
60 |
61 | let to_screen = RectTransform::from_to(
62 | Rect::from_min_size(Pos2::ZERO, response.rect.size()),
63 | response.rect,
64 | );
65 | // let from_screen = to_screen.inverse();
66 |
67 | const MAX_VAL: f32 = 6.;
68 |
69 | let mut points = Vec::new();
70 | let num_pts = data.len() as f32;
71 | // todo: Consider cacheing the calculations here instead of running this each time.
72 | for pt in data {
73 | points.push(
74 | to_screen
75 | * pos2(
76 | pt.0 as f32 / num_pts * width,
77 | // Offset for 0 baseline.
78 | (pt.1 + MAX_VAL / 2.) / MAX_VAL * CHART_HEIGHT,
79 | ),
80 | );
81 | }
82 |
83 | let line = Shape::Path(PathShape::line(points, stroke));
84 |
85 | let mut x_axis = Vec::new();
86 |
87 | if data.len() == 0 {
88 | return;
89 | }
90 |
91 | let data_range = data[data.len() - 1].0 - data[0].0;
92 | let dr_nt = data_range / NUM_X_TICKS;
93 |
94 | let x_axis_posit = CHART_HEIGHT - 4.;
95 |
96 | for i in 0..NUM_X_TICKS {
97 | let tick_v = data[0].0 + dr_nt * i;
98 |
99 | x_axis.push(ui.ctx().fonts(|fonts| {
100 | Shape::text(
101 | fonts,
102 | to_screen * pos2(i as f32 / NUM_X_TICKS as f32 * width, x_axis_posit),
103 | Align2::CENTER_CENTER,
104 | tick_v.to_string(),
105 | FontId::new(14., FontFamily::Proportional),
106 | TICK_COLOR,
107 | )
108 | }));
109 | }
110 |
111 | let mut y_axis = Vec::new();
112 | const NUM_Y_TICKS: usize = 7;
113 | let data_range = 2. * MAX_VAL;
114 | let dr_nt = data_range / NUM_Y_TICKS as f32;
115 |
116 | let y_axis_posit = 4.;
117 |
118 | for i in 0..NUM_Y_TICKS {
119 | let tick_v = -(-MAX_VAL + dr_nt * i as f32) as i8;
120 |
121 | y_axis.push(ui.ctx().fonts(|fonts| {
122 | Shape::text(
123 | fonts,
124 | // to_screen * pos2(y_axis_posit, i as f32 / NUM_Y_TICKS as f32 * CHART_HEIGHT),
125 | to_screen * pos2(y_axis_posit, i as f32 * dr_nt),
126 | Align2::CENTER_CENTER,
127 | tick_v.to_string(),
128 | FontId::new(14., FontFamily::Proportional),
129 | TICK_COLOR,
130 | )
131 | }));
132 | }
133 |
134 | let center_axis = Shape::line_segment(
135 | [
136 | to_screen * pos2(0., CHART_HEIGHT / 2.),
137 | to_screen * pos2(width, CHART_HEIGHT / 2.),
138 | ],
139 | Stroke::new(1., TICK_COLOR),
140 | );
141 |
142 | ui.painter().extend([line]);
143 | ui.painter().extend(x_axis);
144 | // ui.painter().extend(y_axis);
145 | ui.painter().extend([center_axis]);
146 | });
147 | }
148 |
149 | fn pdb_links(data: &PdbData, ui: &mut Ui) {
150 | ui.horizontal(|ui| {
151 | if ui
152 | .button(RichText::new("PDB").color(COLOR_ACTION))
153 | .clicked()
154 | {
155 | rcsb::open_overview(&data.rcsb_id);
156 | }
157 |
158 | if ui.button(RichText::new("3D").color(COLOR_ACTION)).clicked() {
159 | rcsb::open_3d_view(&data.rcsb_id);
160 | }
161 |
162 | if ui
163 | .button(RichText::new("Structure").color(COLOR_ACTION))
164 | .clicked()
165 | {
166 | rcsb::open_structure(&data.rcsb_id);
167 | }
168 | ui.add_space(COL_SPACING / 2.);
169 |
170 | ui.label(
171 | RichText::new(&format!("{}: {}", data.rcsb_id, data.title))
172 | .color(Color32::LIGHT_BLUE)
173 | .font(FontId::monospace(14.)),
174 | );
175 | });
176 | }
177 |
178 | fn draw_proteins(state: &mut State, ui: &mut Ui) {
179 | for protein in &mut state.volatile[state.active].proteins {
180 | ui.horizontal(|ui| {
181 | ui.heading(RichText::new(&protein.feature.label).color(COLOR_INFO));
182 |
183 | ui.add_space(COL_SPACING);
184 |
185 | if ui
186 | .button(RichText::new("PDB search").color(COLOR_ACTION))
187 | .clicked()
188 | {
189 | match rcsb::pdb_data_from_seq(&protein.aa_seq) {
190 | Ok(pdb_data) => {
191 | state.ui.pdb_error_received = false;
192 | protein.pdb_data = pdb_data;
193 | }
194 | Err(_) => {
195 | eprintln!("Error fetching PDB results.");
196 | state.ui.pdb_error_received = true;
197 | }
198 | }
199 | }
200 |
201 | if state.ui.pdb_error_received {
202 | ui.label(RichText::new("Error getting PDB results").color(Color32::LIGHT_RED));
203 | }
204 | //
205 | // if !protein.pdb_ids.is_empty() {
206 | // ui.add_space(COL_SPACING);
207 | // ui.label("Click to open a browser:");
208 | // }
209 | });
210 |
211 | if !protein.pdb_data.is_empty() {
212 | for pdb_result in &protein.pdb_data {
213 | // todo: Use a grid layout or similar; take advantage of horizontal space.
214 | pdb_links(pdb_result, ui);
215 | }
216 | ui.add_space(ROW_SPACING / 2.);
217 | }
218 |
219 | ui.horizontal(|ui| {
220 | ui.label(format!(
221 | "Reading frame: {}, Range: {}",
222 | protein.reading_frame_match.frame, protein.reading_frame_match.range
223 | ));
224 | ui.add_space(COL_SPACING);
225 |
226 | if protein.weight != protein.weight_with_prepost {
227 | // Only show this segment if pre and post-coding sequences exist
228 | ui.label(format!(
229 | "(Coding region only): AA len: {} Weight: {:.1}kDa",
230 | protein.aa_seq.len(),
231 | protein.weight,
232 | ));
233 | ui.add_space(COL_SPACING);
234 | }
235 |
236 | ui.label(format!(
237 | "AA len: {} Weight: {:.1}kDa",
238 | protein.aa_seq.len()
239 | + protein.aa_seq_precoding.len()
240 | + protein.aa_seq_postcoding.len(),
241 | protein.weight_with_prepost,
242 | ));
243 | });
244 | ui.add_space(ROW_SPACING / 2.);
245 |
246 | let aa_text = make_aa_text(&protein.aa_seq, state.ui.aa_ident_disp);
247 | let aa_text_precoding = make_aa_text(&protein.aa_seq_precoding, state.ui.aa_ident_disp);
248 | let aa_text_postcoding = make_aa_text(&protein.aa_seq_postcoding, state.ui.aa_ident_disp);
249 |
250 | if !aa_text_precoding.is_empty() {
251 | ui.label(
252 | RichText::new(aa_text_precoding)
253 | .color(COLOR_PRE_POST_CODING_SEQ)
254 | .font(FontId::new(FONT_SIZE_SEQ, FontFamily::Monospace)),
255 | );
256 | }
257 |
258 | ui.label(
259 | RichText::new(aa_text)
260 | .color(COLOR_PROT_SEQ)
261 | .font(FontId::new(FONT_SIZE_SEQ, FontFamily::Monospace)),
262 | );
263 |
264 | if !aa_text_postcoding.is_empty() {
265 | ui.label(
266 | RichText::new(aa_text_postcoding)
267 | .color(COLOR_PRE_POST_CODING_SEQ)
268 | .font(FontId::new(FONT_SIZE_SEQ, FontFamily::Monospace)),
269 | );
270 | }
271 |
272 | if protein.show_hydropath {
273 | ui.horizontal(|ui| {
274 | ui.heading("Hydrophopathy");
275 | ui.add_space(COL_SPACING);
276 |
277 | ui.label("High values indicate hydrophobic regions; low ones hydrophilic regions.");
278 | ui.add_space(COL_SPACING);
279 |
280 | if ui.button("Hide hydropathy").clicked() {
281 | protein.show_hydropath = false;
282 | }
283 | });
284 |
285 | hydrophobicity_chart(&protein.hydropath_data, ui);
286 | } else {
287 | if ui.button("Show hydrophpathy").clicked() {
288 | protein.show_hydropath = true;
289 | }
290 | }
291 |
292 | ui.add_space(ROW_SPACING * 2.);
293 | }
294 | }
295 |
296 | pub fn protein_page(state: &mut State, ui: &mut Ui) {
297 | ui.horizontal(|ui| {
298 | ui.heading("Proteins, from coding regions");
299 |
300 | ui.add_space(COL_SPACING);
301 | ui.label("One letter ident:");
302 | let mut one_letter = state.ui.aa_ident_disp == AaIdent::OneLetter;
303 | if ui.checkbox(&mut one_letter, "").changed() {
304 | state.ui.aa_ident_disp = if one_letter {
305 | AaIdent::OneLetter
306 | } else {
307 | AaIdent::ThreeLetters
308 | };
309 | }
310 | });
311 | ui.add_space(ROW_SPACING);
312 |
313 | if state.generic[state.active]
314 | .features
315 | .iter()
316 | .any(|f| f.feature_type == FeatureType::CodingRegion)
317 | {
318 | ScrollArea::vertical().id_salt(200).show(ui, |ui| {
319 | draw_proteins(state, ui);
320 | });
321 | } else {
322 | ui.label("Create one or more Coding Region feature to display proteins here.");
323 | }
324 | }
325 |
--------------------------------------------------------------------------------
/src/gui/save.rs:
--------------------------------------------------------------------------------
1 | //! GUI code for saving and loading. Calls business logic in `file_io/save.rs`.
2 |
3 | use std::{env, path::Path};
4 |
5 | use eframe::egui::Ui;
6 | use egui_file_dialog::FileDialog;
7 |
8 | use crate::{
9 | file_io::{
10 | genbank::export_genbank,
11 | save,
12 | save::{StateToSave, export_fasta, load_import},
13 | snapgene::export_snapgene,
14 | },
15 | gui::{navigation::Tab, set_window_title},
16 | state::State,
17 | };
18 |
19 | fn save_button(
20 | dialog: &mut FileDialog,
21 | plasmid_name: &str,
22 | extension: &str,
23 | text: &str,
24 | hover_text: &str,
25 | ui: &mut Ui,
26 | ) {
27 | if ui.button(text).on_hover_text(hover_text).clicked() {
28 | let filename = {
29 | let name = if plasmid_name.is_empty() {
30 | "a_plasmid".to_string()
31 | } else {
32 | plasmid_name.to_lowercase().replace(' ', "_")
33 | };
34 | format!("{name}.{extension}")
35 | };
36 | // save_path.push(Path::new(&filename));
37 |
38 | dialog.config_mut().default_file_name = filename.to_string();
39 | dialog.save_file();
40 | }
41 | }
42 |
43 | fn load_button(dialog: &mut FileDialog, text: &str, hover_text: &str, ui: &mut Ui) {
44 | if ui.button(text).on_hover_text(hover_text).clicked() {
45 | dialog.pick_file();
46 | }
47 | }
48 |
49 | /// Ui elements for saving and loading data in various file formats. This includes our own format,
50 | /// FASTA, and (eventually) SnapGene's DNA format.
51 | pub fn save_section(state: &mut State, ui: &mut Ui) {
52 | let button_text = if state.tabs_open[state.active].path.is_some() {
53 | "Save"
54 | } else {
55 | "Quicksave"
56 | };
57 | if ui
58 | .button(button_text)
59 | .on_hover_text("Save data. (Ctrl + S)")
60 | .clicked()
61 | {
62 | save::save_current_file(state);
63 | }
64 |
65 | save_button(
66 | &mut state.ui.file_dialogs.save,
67 | &state.generic[state.active].metadata.plasmid_name,
68 | "pcad",
69 | "Save as",
70 | "Save data in the PlasCAD format. (Ctrl + Shift + S)",
71 | ui,
72 | );
73 |
74 | load_button(
75 | &mut state.ui.file_dialogs.load,
76 | "Load/Import",
77 | "Load data in the PlasCAD, FASTA, GenBank, or .dna (SnapGene) formats (Ctrl + O)",
78 | ui,
79 | );
80 |
81 | save_button(
82 | &mut state.ui.file_dialogs.export_fasta,
83 | &state.generic[state.active].metadata.plasmid_name,
84 | "fasta",
85 | "Exp FASTA",
86 | "Export the sequence in the FASTA format. This does not include features or primers.",
87 | ui,
88 | );
89 |
90 | save_button(
91 | &mut state.ui.file_dialogs.export_genbank,
92 | &state.generic[state.active].metadata.plasmid_name,
93 | "gbk",
94 | "Exp GenBank",
95 | "Export data in the GenBank format.",
96 | ui,
97 | );
98 |
99 | save_button(
100 | &mut state.ui.file_dialogs.export_dna,
101 | &state.generic[state.active].metadata.plasmid_name,
102 | "dna",
103 | "Exp SnapGene",
104 | "Export data in the .dna (SnapGene) format",
105 | ui,
106 | );
107 |
108 | // todo: DRY.
109 | let ctx = ui.ctx();
110 |
111 | state.ui.file_dialogs.save.update(ctx);
112 | state.ui.file_dialogs.load.update(ctx);
113 | state.ui.file_dialogs.export_fasta.update(ctx);
114 | state.ui.file_dialogs.export_genbank.update(ctx);
115 | state.ui.file_dialogs.export_dna.update(ctx);
116 |
117 | let mut sync = false;
118 |
119 | if let Some(path) = state.ui.file_dialogs.load.take_picked() {
120 | sync = true;
121 | if let Some(loaded) = load_import(&path) {
122 | state.load(&loaded);
123 | }
124 | } else if let Some(path) = state.ui.file_dialogs.save.take_picked() {
125 | match StateToSave::from_state(state, state.active).save_to_file(&path) {
126 | Ok(_) => {
127 | state.tabs_open[state.active] = Tab {
128 | path: Some(path.to_owned()),
129 | ab1: false,
130 | };
131 | set_window_title(&state.tabs_open[state.active], ui);
132 | state.update_save_prefs(); // Save opened tabs.
133 | }
134 | Err(e) => eprintln!("Error saving in PlasCAD format: {:?}", e),
135 | };
136 | } else if let Some(path) = state.ui.file_dialogs.export_fasta.take_picked() {
137 | match export_fasta(
138 | state.get_seq(),
139 | &state.generic[state.active].metadata.plasmid_name,
140 | &path,
141 | ) {
142 | Ok(_) => {
143 | state.tabs_open[state.active] = Tab {
144 | path: Some(path.to_owned()),
145 | ab1: false,
146 | };
147 | set_window_title(&state.tabs_open[state.active], ui);
148 | state.update_save_prefs(); // Save opened tabs.
149 | }
150 | Err(e) => eprintln!("Error exporting to FASTA: {:?}", e),
151 | }
152 | } else if let Some(path) = state.ui.file_dialogs.export_genbank.take_picked() {
153 | let mut primer_matches = Vec::new();
154 | for primer in &state.generic[state.active].primers {
155 | for prim_match in &primer.volatile.matches {
156 | primer_matches.push((prim_match.clone(), primer.name.clone()));
157 | }
158 | }
159 |
160 | match export_genbank(&state.generic[state.active], &primer_matches, &path) {
161 | Ok(_) => {
162 | state.tabs_open[state.active] = Tab {
163 | path: Some(path.to_owned()),
164 | ab1: false,
165 | };
166 | set_window_title(&state.tabs_open[state.active], ui);
167 | state.update_save_prefs(); // Save opened tabs.
168 | }
169 | Err(e) => eprintln!("Error exporting to GenBank: {:?}", e),
170 | }
171 | } else if let Some(path) = state.ui.file_dialogs.export_dna.take_picked() {
172 | match export_snapgene(&state.generic[state.active], &path) {
173 | Ok(_) => {
174 | state.tabs_open[state.active] = Tab {
175 | path: Some(path.to_owned()),
176 | ab1: false,
177 | };
178 | set_window_title(&state.tabs_open[state.active], ui);
179 | state.update_save_prefs(); // Save opened tabs.
180 | }
181 | Err(e) => eprintln!("Error exporting to SnapGene: {:?}", e),
182 | };
183 | }
184 |
185 | if sync {
186 | state.sync_pcr();
187 | state.sync_primer_metrics();
188 | state.sync_seq_related(None);
189 | state.sync_portions();
190 | state.reset_selections();
191 |
192 | set_window_title(&state.tabs_open[state.active], ui);
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/gui/sequence/feature_overlay.rs:
--------------------------------------------------------------------------------
1 | //! This module is related to drawing features on the sequence view. It is similar to `primer_overlay`.
2 |
3 | // todo: Abstract out diffs between this and the primer arrow; avoid repeated code.
4 |
5 | use std::mem;
6 |
7 | use eframe::{
8 | egui::{Align2, Color32, FontFamily, FontId, Pos2, Shape, Stroke, Ui, pos2},
9 | epaint::PathShape,
10 | };
11 |
12 | use crate::{
13 | Selection,
14 | gui::sequence::{
15 | primer_overlay::{HEIGHT, LABEL_OFFSET, SLANT_DIV2, STROKE_WIDTH},
16 | seq_view::{COLOR_CURSOR, NT_WIDTH_PX, SEQ_ROW_SPACING_PX, SeqViewData},
17 | },
18 | misc_types::{
19 | Feature, FeatureDirection,
20 | FeatureDirection::{Forward, Reverse},
21 | FeatureType,
22 | },
23 | util::{RangeIncl, get_feature_ranges},
24 | };
25 |
26 | const VERTICAL_OFFSET_FEATURE: f32 = 18.; // A fudge factor?
27 |
28 | /// We include this in this module because visually, it is very similar to the overlay.
29 | /// Note: At least for now, selection uses 1-based indexing.
30 | pub fn draw_selection(mut selection: RangeIncl, data: &SeqViewData, ui: &mut Ui) -> Vec {
31 | let mut result = Vec::new();
32 |
33 | // This reversal should only occur during reverse dragging; it should resolve when dragging is complete.
34 | if selection.start > selection.end {
35 | mem::swap(&mut selection.start, &mut selection.end);
36 | }
37 |
38 | if selection.start < 1 || selection.end > data.seq_len {
39 | eprintln!("Invalid sequence index");
40 | return result;
41 | }
42 |
43 | // Todo: Cache this, and only update it if row_ranges change. See what else you can optimize
44 | // todo in this way.
45 | let selection_ranges = get_feature_ranges(&selection, &data.row_ranges, data.seq_len);
46 |
47 | let selection_ranges_px: Vec<(Pos2, Pos2)> = selection_ranges
48 | .iter()
49 | .map(|r| (data.seq_i_to_px_rel(r.start), data.seq_i_to_px_rel(r.end)))
50 | .collect();
51 |
52 | result.append(&mut feature_seq_overlay(
53 | &selection_ranges_px,
54 | FeatureType::Selection,
55 | COLOR_CURSOR,
56 | VERTICAL_OFFSET_FEATURE,
57 | FeatureDirection::None,
58 | "",
59 | true,
60 | ui,
61 | ));
62 |
63 | result
64 | }
65 |
66 | pub fn draw_features(
67 | features: &[Feature],
68 | selected_item: Selection,
69 | data: &SeqViewData,
70 | ui: &mut Ui,
71 | ) -> Vec {
72 | let mut result = Vec::new();
73 |
74 | for (i, feature) in features.iter().enumerate() {
75 | // Source features generally take up the whole plasmid length.
76 | // Alternative: Filter by features that take up the whole length.
77 | if feature.feature_type == FeatureType::Source {
78 | continue;
79 | }
80 |
81 | if feature.range.start < 1 {
82 | eprintln!("Invalid sequence index");
83 | continue; // 0 is invalid, in 1-based indexing, and will underflow.
84 | }
85 |
86 | // Todo: Cache this, and only update it if row_ranges change. See what else you can optimize
87 | // todo in this way.
88 | let feature_ranges = get_feature_ranges(&feature.range, &data.row_ranges, data.seq_len);
89 |
90 | let feature_ranges_px: Vec<(Pos2, Pos2)> = feature_ranges
91 | .iter()
92 | .map(|r| (data.seq_i_to_px_rel(r.start), data.seq_i_to_px_rel(r.end)))
93 | .collect();
94 |
95 | let selected = match selected_item {
96 | Selection::Feature(j) => i == j,
97 | _ => false,
98 | };
99 |
100 | let (r, g, b) = feature.color();
101 | let color = Color32::from_rgb(r, g, b);
102 |
103 | result.append(&mut feature_seq_overlay(
104 | &feature_ranges_px,
105 | feature.feature_type,
106 | color,
107 | VERTICAL_OFFSET_FEATURE,
108 | feature.direction,
109 | &feature.label(),
110 | selected,
111 | ui,
112 | ));
113 | }
114 | result
115 | }
116 |
117 | /// Make a visual indicator on the sequence view for a feature, including primers.
118 | /// For use inside a Frame::canvas.
119 | pub fn feature_seq_overlay(
120 | feature_ranges_px: &[(Pos2, Pos2)],
121 | feature_type: FeatureType,
122 | color: Color32,
123 | vertical_offset: f32,
124 | direction: FeatureDirection,
125 | label: &str,
126 | filled: bool,
127 | ui: &mut Ui,
128 | ) -> Vec {
129 | if feature_ranges_px.is_empty() {
130 | return Vec::new();
131 | }
132 | let stroke = Stroke::new(STROKE_WIDTH, color);
133 |
134 | let color_label = Color32::LIGHT_GREEN;
135 |
136 | let v_offset_rev = 2. * vertical_offset;
137 |
138 | // Apply a vertical offset from the sequence.
139 | let feature_ranges_px: Vec<(Pos2, Pos2)> = feature_ranges_px
140 | .iter()
141 | .map(|(start, end)| {
142 | (
143 | pos2(start.x, start.y - vertical_offset),
144 | pos2(end.x, end.y - vertical_offset),
145 | )
146 | })
147 | .collect();
148 |
149 | let mut result = Vec::new();
150 |
151 | // Depends on font size.
152 | let rev_primer_offset = -1.;
153 |
154 | for (i, &(mut start, mut end)) in feature_ranges_px.iter().enumerate() {
155 | // Display the overlay centered around the NT letters, vice above, for non-primer features.
156 | if feature_type != FeatureType::Primer {
157 | start.y += SEQ_ROW_SPACING_PX / 2. - 2.;
158 | end.y += SEQ_ROW_SPACING_PX / 2. - 2.;
159 | }
160 |
161 | let mut top_left = start;
162 | let mut top_right = pos2(end.x + NT_WIDTH_PX, end.y);
163 | let mut bottom_left = pos2(start.x, start.y + HEIGHT);
164 | let mut bottom_right = pos2(end.x + NT_WIDTH_PX, end.y + HEIGHT);
165 |
166 | // Display reverse primers below the sequence; this vertically mirrors.
167 | if feature_type == FeatureType::Primer && direction == Reverse {
168 | top_left.y += 3. * HEIGHT - rev_primer_offset;
169 | top_right.y += 3. * HEIGHT - rev_primer_offset;
170 | bottom_left.y += HEIGHT - rev_primer_offset;
171 | bottom_right.y += HEIGHT - rev_primer_offset;
172 | }
173 |
174 | // Add a slant, if applicable.
175 | match direction {
176 | Forward => {
177 | if i + 1 == feature_ranges_px.len() {
178 | top_right.x -= SLANT_DIV2;
179 | bottom_right.x += SLANT_DIV2;
180 | }
181 | }
182 | Reverse => {
183 | if i == 0 {
184 | top_left.x += SLANT_DIV2;
185 | bottom_left.x -= SLANT_DIV2;
186 | }
187 | }
188 | _ => (),
189 | }
190 |
191 | // let shape = match feature_type {
192 | let shape = if filled {
193 | Shape::Path(PathShape::convex_polygon(
194 | vec![top_left, bottom_left, bottom_right, top_right],
195 | stroke.color,
196 | stroke,
197 | ))
198 | } else {
199 | Shape::Path(PathShape::closed_line(
200 | vec![top_left, bottom_left, bottom_right, top_right],
201 | stroke,
202 | ))
203 | };
204 |
205 | result.push(shape);
206 | }
207 |
208 | // todo: Examine.
209 | let label_start_x = match direction {
210 | Forward => feature_ranges_px[0].0.x,
211 | Reverse => feature_ranges_px[feature_ranges_px.len() - 1].1.x,
212 | FeatureDirection::None => feature_ranges_px[0].0.x,
213 | } + LABEL_OFFSET;
214 |
215 | let mut label_pos = match direction {
216 | Forward => pos2(label_start_x, feature_ranges_px[0].0.y + LABEL_OFFSET),
217 | Reverse => pos2(
218 | label_start_x,
219 | feature_ranges_px[0].0.y + LABEL_OFFSET + v_offset_rev,
220 | ),
221 | FeatureDirection::None => pos2(label_start_x, feature_ranges_px[0].0.y + LABEL_OFFSET), // todo: Examine
222 | };
223 |
224 | let label_align = if feature_type == FeatureType::Primer && direction == Reverse {
225 | label_pos.y -= 2.;
226 | Align2::RIGHT_CENTER
227 | } else {
228 | Align2::LEFT_CENTER
229 | };
230 |
231 | let label = ui.ctx().fonts(|fonts| {
232 | Shape::text(
233 | fonts,
234 | label_pos,
235 | label_align,
236 | label,
237 | FontId::new(13., FontFamily::Proportional),
238 | color_label,
239 | )
240 | });
241 |
242 | result.push(label);
243 | result
244 | }
245 |
--------------------------------------------------------------------------------
/src/gui/sequence/mod.rs:
--------------------------------------------------------------------------------
1 | //! This module contains GUI code related to the sequence view.
2 |
3 | use eframe::egui::{Color32, Frame, RichText, ScrollArea, TextEdit, Ui, text::CursorRange};
4 | use na_seq::{seq_complement, seq_from_str, seq_to_str_lower};
5 |
6 | // todo: monospace font for all seqs.
7 | use crate::gui::{COL_SPACING, ROW_SPACING};
8 | // todo: monospace font for all seqs.
9 | use crate::misc_types::{Feature, FeatureDirection, MIN_SEARCH_LEN};
10 | // todo: monospace font for all seqs.
11 | use crate::state::State;
12 | use crate::{
13 | Selection,
14 | gui::{
15 | PRIMER_FWD_COLOR, SPLIT_SCREEN_MAX_HEIGHT,
16 | circle::feature_range_sliders,
17 | feature_table::{direction_picker, feature_table},
18 | navigation::{PageSeq, PageSeqTop, page_seq_selector, page_seq_top_selector},
19 | primer_table::primer_details,
20 | sequence::seq_view::sequence_vis,
21 | theme::COLOR_ACTION,
22 | },
23 | primer::{Primer, PrimerData},
24 | util::RangeIncl,
25 | };
26 |
27 | mod feature_overlay;
28 | mod primer_overlay;
29 | pub mod seq_view;
30 |
31 | fn seq_editor_raw(state: &mut State, ui: &mut Ui) {
32 | ui.horizontal(|ui| {
33 | ui.heading("Sequence:");
34 | ui.label(&format!("len: {}", state.ui.seq_input.len()));
35 | });
36 |
37 | ScrollArea::vertical().id_salt(200).show(ui, |ui| {
38 | let response = ui.add(TextEdit::multiline(&mut state.ui.seq_input).desired_width(800.));
39 | if response.changed() {
40 | state.generic[state.active].seq = seq_from_str(&state.ui.seq_input);
41 | state.ui.seq_input = seq_to_str_lower(state.get_seq());
42 | state.sync_seq_related(None);
43 | }
44 | });
45 | }
46 |
47 | /// Displays text of the feature under the cursor, or selected, as required.
48 | fn feature_text(i: usize, features: &[Feature], seq_len: usize, ui: &mut Ui) {
49 | if i >= features.len() {
50 | eprintln!("Invalid selected feature");
51 | return; // todo: Ideally set the feature to none.
52 | }
53 | let feature = &features[i];
54 |
55 | ui.label(&feature.label);
56 | ui.label(feature.location_descrip(seq_len));
57 | let (r, g, b) = feature.color();
58 | ui.label(RichText::new(feature.feature_type.to_string()).color(Color32::from_rgb(r, g, b)));
59 |
60 | // todo?
61 | for note in &feature.notes {
62 | // ui.label(&format!("{}: {}", note.0, note.1));
63 | }
64 | }
65 |
66 | fn primer_text(i: usize, primers: &[Primer], seq_len: usize, ui: &mut Ui) {
67 | if i >= primers.len() {
68 | eprintln!("Invalid selected primer");
69 | return; // todo: Ideally set the feature to none.
70 | }
71 | let primer = &primers[i];
72 |
73 | ui.label(&primer.name);
74 | ui.label(&primer.location_descrip());
75 | // todo: Rev color A/R
76 | ui.label(RichText::new(seq_to_str_lower(&primer.sequence)).color(PRIMER_FWD_COLOR));
77 |
78 | ui.label(&primer.description.clone().unwrap_or_default());
79 | }
80 |
81 | /// Add a toolbar to create a feature from selection, if appropriate.
82 | fn feature_from_sel(state: &mut State, ui: &mut Ui) {
83 | if let Some(text_sel) = state.ui.text_selection {
84 | if ui
85 | .button(RichText::new("➕ Add feature from sel").color(COLOR_ACTION))
86 | .clicked()
87 | {
88 | state.generic[state.active].features.push(Feature {
89 | range: text_sel,
90 | label: state.ui.quick_feature_add_name.clone(),
91 | direction: state.ui.quick_feature_add_dir,
92 | ..Default::default()
93 | });
94 |
95 | state.ui.text_selection = None;
96 | state.ui.quick_feature_add_name = String::new();
97 | }
98 |
99 | if ui
100 | .button(RichText::new("➕ Add primer from sel").color(COLOR_ACTION))
101 | .clicked()
102 | {
103 | // todo: DRY with genbank parsing; common fn A/R.
104 | let seq = state.get_seq();
105 | let compl = &seq_complement(seq);
106 | let seq_primer = match state.ui.quick_feature_add_dir {
107 | FeatureDirection::Reverse => {
108 | let range = RangeIncl::new(
109 | seq.len() - text_sel.end + 1,
110 | seq.len() - text_sel.start + 1,
111 | );
112 |
113 | range.index_seq(compl).unwrap_or_default()
114 | }
115 | _ => text_sel.index_seq(seq).unwrap_or_default(),
116 | }
117 | .to_vec();
118 |
119 | let volatile = PrimerData::new(&seq_primer);
120 |
121 | state.generic[state.active].primers.push(Primer {
122 | sequence: seq_primer,
123 | name: state.ui.quick_feature_add_name.clone(),
124 | description: None,
125 | volatile,
126 | });
127 |
128 | state.ui.quick_feature_add_name = String::new();
129 | state.sync_primer_matches(None);
130 | state.sync_primer_metrics();
131 | }
132 |
133 | direction_picker(&mut state.ui.quick_feature_add_dir, 200, ui);
134 |
135 | ui.label("Name:");
136 | if ui
137 | .add(TextEdit::singleline(&mut state.ui.quick_feature_add_name).desired_width(80.))
138 | .gained_focus()
139 | {
140 | state.ui.text_edit_active = true; // Disable character entries in the sequence.
141 | }
142 |
143 | ui.add_space(COL_SPACING)
144 | }
145 | }
146 |
147 | /// Component for the sequence page.
148 | pub fn seq_page(state: &mut State, ui: &mut Ui) {
149 | ui.horizontal(|ui| {
150 | page_seq_top_selector(state, ui);
151 |
152 | ui.add_space(COL_SPACING);
153 |
154 | feature_from_sel(state, ui);
155 |
156 | // Sliders to edit the feature.
157 | feature_range_sliders(state, ui);
158 | });
159 |
160 | // Limit the top section height.
161 | let screen_height = ui.ctx().available_rect().height();
162 | let half_screen_height = screen_height / SPLIT_SCREEN_MAX_HEIGHT;
163 |
164 | match state.ui.page_seq_top {
165 | PageSeqTop::Primers => {
166 | Frame::none().show(ui, |ui| {
167 | ScrollArea::vertical()
168 | .max_height(half_screen_height)
169 | .id_salt(69)
170 | .show(ui, |ui| primer_details(state, ui));
171 | });
172 | }
173 | PageSeqTop::Features => {
174 | Frame::none().show(ui, |ui| {
175 | ScrollArea::vertical()
176 | .max_height(half_screen_height)
177 | .id_salt(70)
178 | .show(ui, |ui| {
179 | feature_table(state, ui);
180 | });
181 | });
182 | }
183 | PageSeqTop::None => (),
184 | }
185 |
186 | ui.add_space(ROW_SPACING);
187 |
188 | ui.horizontal(|ui| {
189 | page_seq_selector(state, ui);
190 | ui.add_space(COL_SPACING);
191 |
192 | ui.label("🔍").on_hover_text(
193 | "Search the sequence and its complement for this term. (Ctrl + F to highlight)",
194 | );
195 |
196 | // This nonstandard way of adding the text input is required for the auto-highlight on ctrl+F behavior.
197 | let mut output = TextEdit::singleline(&mut state.ui.search_input)
198 | .desired_width(400.)
199 | .show(ui);
200 | let response = output.response;
201 |
202 | if state.ui.highlight_search_input {
203 | state.ui.highlight_search_input = false;
204 | state.ui.text_edit_active = true; // Disable character entries in the sequence.
205 | response.request_focus();
206 |
207 | output.cursor_range = Some(CursorRange::select_all(&output.galley));
208 | // todo: Not working
209 | }
210 |
211 | if response.gained_focus() {
212 | state.ui.text_edit_active = true; // Disable character entries in the sequence.
213 | println!("GF");
214 | }
215 |
216 | if response.changed() {
217 | state.ui.text_edit_active = true;
218 | state.search_seq = seq_from_str(&state.ui.search_input);
219 | state.ui.search_input = seq_to_str_lower(&state.search_seq); // Ensures only valid NTs are present.
220 |
221 | // todo: This still adds a single char, then blanks the cursor...
222 | state.ui.text_cursor_i = None; // Make sure we are not adding chars.
223 | state.sync_search();
224 | };
225 |
226 | if state.ui.search_input.len() >= MIN_SEARCH_LEN {
227 | let len = state.volatile[state.active].search_matches.len();
228 | let text = if len == 1 {
229 | "1 match".to_string()
230 | } else {
231 | format!("{} matches", len)
232 | };
233 | ui.label(text);
234 | }
235 |
236 | ui.add_space(COL_SPACING);
237 |
238 | let mut feature_to_disp = None;
239 | let mut primer_to_disp = None;
240 |
241 | match state.ui.selected_item {
242 | Selection::Feature(i) => feature_to_disp = Some(i),
243 | Selection::Primer(i) => primer_to_disp = Some(i),
244 | Selection::None => {
245 | if state.ui.feature_hover.is_some() {
246 | feature_to_disp = Some(state.ui.feature_hover.unwrap());
247 | }
248 | }
249 | }
250 |
251 | if let Some(feature_i) = feature_to_disp {
252 | feature_text(
253 | feature_i,
254 | &state.generic[state.active].features,
255 | state.get_seq().len(),
256 | ui,
257 | );
258 | }
259 |
260 | if let Some(primer_i) = primer_to_disp {
261 | primer_text(
262 | primer_i,
263 | &state.generic[state.active].primers,
264 | state.get_seq().len(),
265 | ui,
266 | );
267 | }
268 | });
269 |
270 | ui.add_space(ROW_SPACING / 2.);
271 |
272 | ScrollArea::vertical()
273 | .id_salt(100)
274 | .show(ui, |ui| match state.ui.page_seq {
275 | PageSeq::EditRaw => {
276 | state.ui.text_cursor_i = None; // prevents double-edits
277 | seq_editor_raw(state, ui);
278 | }
279 | PageSeq::View => {
280 | sequence_vis(state, ui);
281 | }
282 | });
283 | }
284 |
--------------------------------------------------------------------------------
/src/gui/sequence/primer_overlay.rs:
--------------------------------------------------------------------------------
1 | //! This module contains code related to drawing primer arrows in the sequence view.
2 |
3 | use eframe::egui::{Pos2, Shape, Ui};
4 |
5 | use crate::{
6 | Selection,
7 | gui::sequence::{feature_overlay, seq_view::SeqViewData},
8 | misc_types::FeatureType,
9 | primer::{Primer, PrimerDirection},
10 | util,
11 | util::RangeIncl,
12 | };
13 |
14 | pub const STROKE_WIDTH: f32 = 2.;
15 |
16 | pub const VERTICAL_OFFSET_PRIMER: f32 = 18.; // A fudge factor?
17 | pub const LABEL_OFFSET: f32 = 7.;
18 | pub const HEIGHT: f32 = 16.;
19 | pub const SLANT: f32 = 12.; // slant different, in pixels, for the arrow.
20 | pub const SLANT_DIV2: f32 = SLANT / 2.;
21 |
22 | /// Add primer arrows to the display.
23 | pub fn draw_primers(
24 | primers: &[Primer],
25 | selected_item: Selection,
26 | data: &SeqViewData,
27 | ui: &mut Ui,
28 | ) -> Vec {
29 | let mut shapes = Vec::new();
30 |
31 | for (i, primer) in primers.iter().enumerate() {
32 | let primer_matches = &primer.volatile.matches;
33 |
34 | // todo: Do not run these calcs each time. Cache.
35 | for prim_match in primer_matches {
36 | // We currently index primers relative to the end they started.
37 |
38 | // Note: Because if we're displaying above the seq and below, the base of the arrow must match,
39 | // hence the offset.
40 | let seq_range = match prim_match.direction {
41 | PrimerDirection::Forward => {
42 | // todo: Getting an underflow, but not sure why yet.
43 | let end = if prim_match.range.end > 0 {
44 | prim_match.range.end - 1
45 | } else {
46 | prim_match.range.end
47 | };
48 | RangeIncl::new(prim_match.range.start, end)
49 | }
50 |
51 | PrimerDirection::Reverse => {
52 | RangeIncl::new(prim_match.range.start + 1, prim_match.range.end)
53 | }
54 | };
55 |
56 | let feature_ranges =
57 | util::get_feature_ranges(&seq_range, &data.row_ranges, data.seq_len);
58 |
59 | let feature_ranges_px: Vec<(Pos2, Pos2)> = feature_ranges
60 | .iter()
61 | .map(|r| (data.seq_i_to_px_rel(r.start), data.seq_i_to_px_rel(r.end)))
62 | .collect();
63 |
64 | let color = prim_match.direction.color();
65 |
66 | let selected = match selected_item {
67 | Selection::Primer(j) => i == j,
68 | _ => false,
69 | };
70 |
71 | // todo: PUt back; temp check on compiling.
72 | shapes.append(&mut feature_overlay::feature_seq_overlay(
73 | &feature_ranges_px,
74 | FeatureType::Primer,
75 | color,
76 | VERTICAL_OFFSET_PRIMER,
77 | (prim_match.direction).into(),
78 | &primer.name,
79 | selected,
80 | ui,
81 | ));
82 | }
83 | }
84 | shapes
85 | }
86 |
--------------------------------------------------------------------------------
/src/gui/theme.rs:
--------------------------------------------------------------------------------
1 | use eframe::egui::Color32;
2 |
3 | pub const COLOR_ACTION: Color32 = Color32::GOLD;
4 | pub const COLOR_INFO: Color32 = Color32::LIGHT_BLUE;
5 |
--------------------------------------------------------------------------------
/src/ligation.rs:
--------------------------------------------------------------------------------
1 | use na_seq::restriction_enzyme::RestrictionEnzyme;
2 |
3 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | // Disables the terminal window on Windows, in release mode.
2 | #![cfg_attr(
3 | all(not(debug_assertions), target_os = "windows"),
4 | windows_subsystem = "windows"
5 | )]
6 |
7 | // todo: Build a database of feature sequences. You can find GenBank etc files online (addGene, and other sources)
8 | // todo: and parse common features.
9 |
10 | // todo: Break out Generic into its own mod?
11 |
12 | // todo:
13 | // Focus on tools that allow you to conveniently design plasmid seqs based on source vectors, REs etc. It will make primers,
14 | // choose how to combine sequences etc. so, more towards realistic, product-first workflows.
15 | //
16 | // This will make more sense when you are designing new products.
17 | // For example: consider a lib of Addgene's generic vectors for E. Coli.
18 | //
19 | // The input: your target product: Output: as much we can automate as possible.
20 |
21 | // Reading frame: Guess the frame, and truncate the start based on CodingRegion and Gene feature types?
22 | use std::{env, path::PathBuf, str::FromStr};
23 |
24 | use bincode::{Decode, Encode};
25 | use cloning::CloningInsertData;
26 | use copypasta::ClipboardProvider;
27 | use eframe::{
28 | self,
29 | egui::{self},
30 | };
31 | use file_io::save::{QUICKSAVE_FILE, load_import};
32 | use gui::navigation::{Page, PageSeq};
33 | use na_seq::{AaIdent, Nucleotide, Seq, restriction_enzyme::RestrictionEnzyme};
34 | use state::State;
35 |
36 | use crate::{
37 | backbones::BackboneFilters,
38 | file_io::{FileDialogs, save::DEFAULT_PREFS_FILE},
39 | gui::{
40 | WINDOW_HEIGHT, WINDOW_WIDTH,
41 | navigation::{PageSeqTop, Tab},
42 | },
43 | misc_types::{FeatureDirection, FeatureType},
44 | pcr::PcrUi,
45 | primer::TM_TARGET,
46 | util::{RangeIncl, get_window_title},
47 | };
48 |
49 | mod ab1;
50 | mod alignment;
51 | mod alignment_map;
52 | mod backbones;
53 | mod cloning;
54 | mod external_websites;
55 | mod feature_db_load;
56 | mod file_io;
57 | mod gui;
58 | mod melting_temp_calcs;
59 | mod misc_types;
60 | mod pcr;
61 | mod portions;
62 | mod primer;
63 | mod primer_metrics;
64 | mod protein;
65 | mod reading_frame;
66 | mod save_compat;
67 | mod solution_helper;
68 | mod state;
69 | mod tags;
70 | mod toxic_proteins;
71 | mod util;
72 |
73 | type Color = (u8, u8, u8); // RGB
74 |
75 | // todo: Eventually, implement a system that automatically checks for changes, and don't
76 | // todo save to disk if there are no changes.
77 | const PREFS_SAVE_INTERVAL: u64 = 60; // Save user preferences this often, in seconds.
78 |
79 | #[derive(Default, Encode, Decode)]
80 | struct StateFeatureAdd {
81 | // This is in 1-based indexing.
82 | start_posit: usize,
83 | end_posit: usize,
84 | feature_type: FeatureType,
85 | direction: FeatureDirection,
86 | label: String,
87 | color: Option,
88 | }
89 |
90 | #[derive(Clone, Encode, Decode)]
91 | /// This Ui struct is used to determine which items on the sequence and map views to show and hide.
92 | struct SeqVisibility {
93 | /// Show or hide restriction enzymes from the sequence view.
94 | show_res: bool,
95 | /// Show and hide primers on
96 | show_primers: bool,
97 | /// todo: Show and hide individual features?
98 | show_features: bool,
99 | show_reading_frame: bool,
100 | }
101 |
102 | impl Default for SeqVisibility {
103 | fn default() -> Self {
104 | Self {
105 | show_res: true,
106 | show_primers: true,
107 | show_features: true,
108 | show_reading_frame: false,
109 | }
110 | }
111 | }
112 |
113 | /// UI state for restriction enzymes.
114 | pub struct ReUi {
115 | /// Inner: RE name
116 | /// todo: This is a trap for multiple tabs.
117 | // res_selected: Vec,
118 | res_selected: Vec,
119 | /// Which tabs' sequences to digest. Note that the index of this vec doesn't matter; the values, which
120 | /// point to indices elsewhere, does.
121 | tabs_selected: Vec,
122 | unique_cutters_only: bool,
123 | /// No blunt ends; must produce overhangs.
124 | sticky_ends_only: bool,
125 | /// Only show REs that are present in at least two sequences.
126 | multiple_seqs: bool,
127 | }
128 |
129 | impl Default for ReUi {
130 | fn default() -> Self {
131 | Self {
132 | res_selected: Default::default(),
133 | tabs_selected: Default::default(),
134 | unique_cutters_only: true,
135 | sticky_ends_only: false,
136 | multiple_seqs: true,
137 | }
138 | }
139 | }
140 |
141 | /// Values defined here generally aren't worth saving to file etc.
142 | struct StateUi {
143 | // todo: Make separate primer cols and primer data; data in state. primer_cols are pre-formatted
144 | // todo to save computation.
145 | page: Page,
146 | page_seq: PageSeq,
147 | page_seq_top: PageSeqTop,
148 | seq_input: String, // todo: Consider moving this to volatile.
149 | pcr: PcrUi,
150 | feature_add: StateFeatureAdd,
151 | // primer_selected: Option, // primer page only.
152 | feature_hover: Option, // todo: Apply similar enum logic to selection: Allow Primer::, Feature::, or None::
153 | selected_item: Selection,
154 | seq_visibility: SeqVisibility,
155 | hide_map_feature_editor: bool,
156 | /// Mouse cursor
157 | cursor_pos: Option<(f32, f32)>,
158 | /// Mouse cursor
159 | cursor_seq_i: Option,
160 | file_dialogs: FileDialogs,
161 | /// Show or hide the field to change origin
162 | show_origin_change: bool,
163 | new_origin: usize,
164 | /// Text-editing cursor. Used for editing on the sequence view. Chars typed
165 | /// will be inserted after this index. This index is 0-based.
166 | text_cursor_i: Option,
167 | /// We store if we've clicked somewhere separately from the action, as getting a sequence index
168 | /// from cursor positions may be decoupled, and depends on the view.
169 | click_pending_handle: bool,
170 | /// We use this for selecting features from the seq view
171 | dblclick_pending_handle: bool,
172 | cloning_insert: CloningInsertData,
173 | /// Volatile; computed dynamically based on window size.
174 | nt_chars_per_row: usize,
175 | search_input: String,
176 | /// Used to trigger a search focus on hitting ctrl+f
177 | highlight_search_input: bool,
178 | /// Activated when the user selects the search box; disables character insertion.
179 | text_edit_active: bool,
180 | /// This is used for selecting nucleotides on the sequence viewer.
181 | dragging: bool,
182 | /// 1-based indexing.
183 | text_selection: Option,
184 | quick_feature_add_name: String,
185 | quick_feature_add_dir: FeatureDirection,
186 | // todo: Protein ui A/R
187 | aa_ident_disp: AaIdent,
188 | pdb_error_received: bool,
189 | re: ReUi,
190 | backbone_filters: BackboneFilters,
191 | seq_edit_lock: bool,
192 | ab1_start_i: usize,
193 | }
194 |
195 | impl Default for StateUi {
196 | fn default() -> Self {
197 | Self {
198 | page: Default::default(),
199 | page_seq: Default::default(),
200 | page_seq_top: Default::default(),
201 | seq_input: Default::default(),
202 | pcr: Default::default(),
203 | feature_add: Default::default(),
204 | feature_hover: Default::default(),
205 | selected_item: Default::default(),
206 | seq_visibility: Default::default(),
207 | hide_map_feature_editor: true,
208 | cursor_pos: Default::default(),
209 | cursor_seq_i: Default::default(),
210 | file_dialogs: Default::default(),
211 | show_origin_change: Default::default(),
212 | new_origin: Default::default(),
213 | text_cursor_i: Some(0),
214 | click_pending_handle: Default::default(),
215 | dblclick_pending_handle: Default::default(),
216 | cloning_insert: Default::default(),
217 | nt_chars_per_row: Default::default(),
218 | search_input: Default::default(),
219 | highlight_search_input: Default::default(),
220 | text_edit_active: Default::default(),
221 | dragging: Default::default(),
222 | text_selection: Default::default(),
223 | quick_feature_add_name: Default::default(),
224 | quick_feature_add_dir: Default::default(),
225 | aa_ident_disp: AaIdent::OneLetter,
226 | pdb_error_received: false,
227 | re: Default::default(),
228 | backbone_filters: Default::default(),
229 | seq_edit_lock: true,
230 | ab1_start_i: Default::default(),
231 | }
232 | }
233 | }
234 |
235 | #[derive(Clone, Copy, PartialEq, Debug, Encode, Decode)]
236 | pub enum Selection {
237 | Feature(usize), // index
238 | Primer(usize),
239 | None,
240 | }
241 |
242 | impl Default for Selection {
243 | fn default() -> Self {
244 | Self::None
245 | }
246 | }
247 |
248 | fn main() {
249 | let mut state = State::default();
250 |
251 | // todo: Temp to test BAM
252 | // let am = alignment_map::import(&PathBuf::from_str("../../Desktop/test.bam").unwrap()).unwrap();
253 | // println!("Alignment map loaded: {:?}", am);
254 |
255 | state.load_prefs(&PathBuf::from_str(DEFAULT_PREFS_FILE).unwrap());
256 |
257 | // Initial load hierarchy:
258 | // - Path argument (e.g. file association)
259 | // - Last opened files
260 | // - Quicksave
261 |
262 | let mut loaded_from_arg = false;
263 | let (path, window_title_initial) = {
264 | let mut p = PathBuf::from_str(QUICKSAVE_FILE).unwrap();
265 |
266 | // Windows and possibly other operating systems, if attempting to use your program to natively
267 | // open a file type, will use command line arguments to indicate this. Determine if the program
268 | // is being launched this way, and if so, open the file.
269 | let args: Vec = env::args().collect();
270 | if args.len() > 1 {
271 | let temp = &args[1];
272 | p = PathBuf::from_str(temp).unwrap();
273 | loaded_from_arg = true;
274 | }
275 |
276 | let window_title = get_window_title(&p);
277 | (p, window_title)
278 | };
279 |
280 | let mut prev_paths_loaded = false;
281 | for tab in &state.tabs_open {
282 | if tab.path.is_some() {
283 | prev_paths_loaded = true;
284 | }
285 | }
286 |
287 | // todo: Consider a standalone method for loaded-from-arg,
288 |
289 | // Load from the argument or quicksave A/R.
290 | if loaded_from_arg || !prev_paths_loaded {
291 | println!("Loading from quicksave or arg: {:?}", path);
292 | if let Some(loaded) = load_import(&path) {
293 | state.load(&loaded);
294 | }
295 | }
296 |
297 | state.sync_seq_related(None);
298 |
299 | state.reset_selections();
300 |
301 | let icon_bytes: &[u8] = include_bytes!("resources/icon.png");
302 | let icon_data = eframe::icon_data::from_png_bytes(icon_bytes);
303 |
304 | let options = eframe::NativeOptions {
305 | viewport: egui::ViewportBuilder::default()
306 | .with_inner_size([WINDOW_WIDTH, WINDOW_HEIGHT])
307 | .with_icon(icon_data.unwrap()),
308 | ..Default::default()
309 | };
310 |
311 | eframe::run_native(
312 | &window_title_initial,
313 | options,
314 | Box::new(|_cc| Ok(Box::new(state))),
315 | )
316 | .unwrap();
317 | }
318 |
--------------------------------------------------------------------------------
/src/misc_types.rs:
--------------------------------------------------------------------------------
1 | //! This module contains fundamental data structures, eg related to features, metadata, etc.
2 |
3 | use bincode::{Decode, Encode};
4 | use na_seq::Nucleotide;
5 |
6 | use crate::{
7 | Color,
8 | primer::PrimerDirection,
9 | util::{RangeIncl, match_subseq},
10 | };
11 | pub const MIN_SEARCH_LEN: usize = 3;
12 |
13 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
14 | pub enum FeatureType {
15 | Generic,
16 | Gene,
17 | Ori,
18 | RibosomeBindSite,
19 | Promoter,
20 | AntibioticResistance,
21 | /// Note: This one behaves a bit different from the others; we use it here so we can share the feature
22 | /// overlay code.
23 | Primer,
24 | /// Ie, a gene.
25 | CodingRegion,
26 | LongTerminalRepeat,
27 | /// We don't draw these on the map or sequence views; found in GenBank formats (at least), these
28 | /// are the range of the entire sequence.
29 | Source,
30 | Exon,
31 | Transcript,
32 | /// Like Primer, this is not a real feature; we use it to draw the selection highlighted area.
33 | Selection,
34 | /// Ie operators.
35 | ProteinBind,
36 | Terminator,
37 | }
38 |
39 | impl Default for FeatureType {
40 | fn default() -> Self {
41 | Self::Generic
42 | }
43 | }
44 |
45 | impl FeatureType {
46 | /// For displaying in the UI
47 | pub fn to_string(self) -> String {
48 | match self {
49 | Self::Generic => "Generic",
50 | Self::Gene => "Gene",
51 | Self::Ori => "Origin of replication",
52 | Self::RibosomeBindSite => "Ribosome bind site",
53 | Self::Promoter => "Promoter",
54 | Self::AntibioticResistance => "Antibiotic resistance",
55 | Self::Primer => "Primer",
56 | Self::CodingRegion => "Coding region",
57 | Self::LongTerminalRepeat => "Long term repeat",
58 | Self::Source => "Source",
59 | Self::Exon => "Exon",
60 | Self::Transcript => "Transcript",
61 | Self::Selection => "",
62 | Self::ProteinBind => "Operator",
63 | Self::Terminator => "Terminator",
64 | }
65 | .to_owned()
66 | }
67 |
68 | pub fn color(&self) -> Color {
69 | match self {
70 | Self::Generic => (255, 0, 255),
71 | Self::Gene => (255, 128, 128),
72 | Self::Ori => (40, 200, 128),
73 | Self::RibosomeBindSite => (255, 204, 252),
74 | Self::Promoter => (240, 190, 70),
75 | Self::AntibioticResistance => (0, 200, 110),
76 | Self::Primer => (0, 0, 0), // N/A for now at least.
77 | Self::CodingRegion => (100, 200, 255), // N/A for now at least.
78 | Self::LongTerminalRepeat => (150, 200, 255), // N/A for now at least.
79 | Self::Source => (120, 70, 120),
80 | Self::Exon => (255, 255, 180),
81 | Self::Transcript => (180, 255, 180),
82 | Self::Selection => (255, 255, 0),
83 | Self::ProteinBind => (128, 110, 150),
84 | Self::Terminator => (255, 110, 150),
85 | }
86 | }
87 |
88 | /// Parse from a string; we use this for both SnapGene and GenBank.
89 | pub fn from_external_str(v: &str) -> Self {
90 | // todo: Update as required with more
91 | let v = &v.to_lowercase();
92 |
93 | match v.as_ref() {
94 | "cds" => Self::CodingRegion,
95 | "gene" => Self::Gene,
96 | "rbs" => Self::RibosomeBindSite,
97 | "rep_origin" => Self::Ori,
98 | "promoter" => Self::Promoter,
99 | "primer_bind" => Self::Primer, // todo: This is a bit awk; genbank.
100 | "ltr" => Self::LongTerminalRepeat,
101 | "misc_feature" => Self::Generic,
102 | "source" => Self::Source,
103 | "exon" => Self::Exon,
104 | "transcript" => Self::Transcript,
105 | "protein_bind" => Self::ProteinBind,
106 | "terminator" => Self::Terminator,
107 | _ => Self::Generic,
108 | }
109 | }
110 |
111 | /// Create a string for use with SnapGene and GenBank formats.
112 | pub fn to_external_str(self) -> String {
113 | // todo: Update as required with more
114 | match self {
115 | Self::Generic => "misc_feature",
116 | Self::Gene => "gene",
117 | Self::Ori => "rep_origin",
118 | Self::RibosomeBindSite => "rbs",
119 | Self::Promoter => "promoter",
120 | Self::AntibioticResistance => "antibiotic resistance", // todo
121 | Self::Primer => "primer_bind",
122 | Self::CodingRegion => "cds",
123 | Self::LongTerminalRepeat => "ltr",
124 | Self::Source => "source",
125 | Self::Exon => "exon",
126 | Self::Transcript => "transcript",
127 | Self::Selection => "",
128 | Self::ProteinBind => "protein_bind",
129 | Self::Terminator => "terminator",
130 | }
131 | .to_string()
132 | }
133 | }
134 |
135 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
136 | pub enum FeatureDirection {
137 | None,
138 | Forward,
139 | Reverse,
140 | }
141 |
142 | impl From for FeatureDirection {
143 | fn from(value: PrimerDirection) -> Self {
144 | match value {
145 | PrimerDirection::Forward => Self::Forward,
146 | PrimerDirection::Reverse => Self::Reverse,
147 | }
148 | }
149 | }
150 |
151 | impl Default for FeatureDirection {
152 | fn default() -> Self {
153 | Self::None
154 | }
155 | }
156 |
157 | impl FeatureDirection {
158 | pub fn to_string(self) -> String {
159 | match self {
160 | Self::None => "None",
161 | Self::Forward => "Forward",
162 | Self::Reverse => "Reverse",
163 | }
164 | .to_owned()
165 | }
166 | }
167 |
168 | #[derive(Clone, Encode, Decode)]
169 | pub struct Feature {
170 | // pub range: (usize, usize),
171 | /// 1-based indexing, inclusive. (Note: Could also use the builtin RangeInclusive.)
172 | pub range: RangeIncl,
173 | pub feature_type: FeatureType,
174 | pub direction: FeatureDirection,
175 | pub label: String,
176 | /// By default, we display features using featuretype-specific color. Allow the user
177 | /// to override this.
178 | pub color_override: Option,
179 | pub notes: Vec<(String, String)>,
180 | }
181 |
182 | impl Default for Feature {
183 | fn default() -> Self {
184 | Self {
185 | range: RangeIncl::new(1, 1),
186 | feature_type: Default::default(),
187 | direction: Default::default(),
188 | label: Default::default(),
189 | color_override: Default::default(),
190 | notes: Default::default(),
191 | }
192 | }
193 | }
194 |
195 | impl Feature {
196 | pub fn label(&self) -> String {
197 | if self.label.is_empty() {
198 | self.feature_type.to_string()
199 | } else {
200 | self.label.clone()
201 | }
202 | }
203 |
204 | /// Get the color to draw; type color, unless overridden.
205 | pub fn color(&self) -> Color {
206 | match self.color_override {
207 | Some(c) => c,
208 | None => self.feature_type.color(),
209 | }
210 | }
211 |
212 | /// Get the feature len, in usize.
213 | pub fn len(&self, seq_len: usize) -> usize {
214 | if self.range.end > self.range.start {
215 | self.range.end - self.range.start + 1
216 | } else {
217 | // ie a wrap through the origin
218 | self.range.end + seq_len - self.range.start + 1
219 | }
220 | }
221 |
222 | /// Formats the indexes, and size of this feature.
223 | pub fn location_descrip(&self, seq_len: usize) -> String {
224 | format!(
225 | "{}..{} {} bp",
226 | self.range.start,
227 | self.range.end,
228 | self.len(seq_len)
229 | )
230 | }
231 | }
232 |
233 | /// Contains sequence-level metadata.
234 | #[derive(Clone, Default, Encode, Decode)]
235 | pub struct Metadata {
236 | pub plasmid_name: String,
237 | pub comments: Vec,
238 | pub references: Vec,
239 | pub locus: String,
240 | // pub date: Option,
241 | /// We use this to prevent manual encode/decode for the whoel struct.
242 | pub date: Option<(i32, u8, u8)>, // Y M D
243 | pub definition: Option,
244 | pub molecule_type: Option,
245 | pub division: String,
246 | pub accession: Option,
247 | pub version: Option,
248 | // pub keywords: Vec,
249 | pub keywords: Option, // todo vec?
250 | pub source: Option,
251 | pub organism: Option,
252 | }
253 |
254 | /// Based on GenBank's reference format
255 | #[derive(Default, Clone, Encode, Decode)]
256 | pub struct Reference {
257 | pub description: String,
258 | pub authors: Option,
259 | pub consortium: Option,
260 | pub title: String,
261 | pub journal: Option,
262 | pub pubmed: Option,
263 | pub remark: Option,
264 | }
265 |
266 | pub struct SearchMatch {
267 | /// 0-based indexing.
268 | pub range: RangeIncl,
269 | // todo: More A/R
270 | }
271 |
272 | // todo: Should this go to the `seq` library?
273 | /// Find exact matches in the target sequence of our search nucleotides.
274 | /// todo: Optionally support partial matches.
275 | /// todo:
276 | pub fn find_search_matches(seq: &[Nucleotide], search_seq: &[Nucleotide]) -> Vec {
277 | let (mut fwd, mut rev) = match_subseq(search_seq, seq);
278 |
279 | fwd.append(&mut rev);
280 | fwd.into_iter().map(|range| SearchMatch { range }).collect()
281 | }
282 |
--------------------------------------------------------------------------------
/src/pcr.rs:
--------------------------------------------------------------------------------
1 | //! This module assists in identifying PCR parameters
2 |
3 | use bincode::{Decode, Encode};
4 | use na_seq::Seq;
5 |
6 | use crate::{
7 | gui::navigation::{Page, PageSeq, Tab},
8 | primer::{Primer, TM_TARGET},
9 | state::State,
10 | util::RangeIncl,
11 | };
12 |
13 | #[derive(Clone, Encode, Decode)]
14 | /// Variables for UI fields, for determining PCR parameters.
15 | pub struct PcrUi {
16 | pub primer_tm: f32,
17 | pub product_len: usize,
18 | pub polymerase_type: PolymeraseType,
19 | pub num_cycles: u16,
20 | /// Index from primer data for the load-from-primer system. For storing dropdown state.
21 | pub primer_selected: usize,
22 | /// These are for the PCR product generation
23 | pub primer_fwd: usize,
24 | pub primer_rev: usize,
25 | }
26 |
27 | impl Default for PcrUi {
28 | fn default() -> Self {
29 | Self {
30 | primer_tm: TM_TARGET,
31 | product_len: 1_000,
32 | polymerase_type: Default::default(),
33 | num_cycles: 30,
34 | primer_selected: 0,
35 | primer_fwd: 0,
36 | primer_rev: 0,
37 | }
38 | }
39 | }
40 |
41 | /// This is a common pattern for PCR parameters
42 | #[derive(Default, Encode, Decode)]
43 | pub struct TempTime {
44 | /// In °C
45 | pub temp: f32,
46 | /// In seconds
47 | pub time: u16,
48 | }
49 |
50 | impl TempTime {
51 | pub fn new(temp: f32, time: u16) -> Self {
52 | Self { temp, time }
53 | }
54 | }
55 |
56 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
57 | pub enum PolymeraseType {
58 | NormalFidelity,
59 | /// Eg Phusion; results in a shorter extension time.
60 | HighFidelity,
61 | }
62 |
63 | impl Default for PolymeraseType {
64 | fn default() -> Self {
65 | Self::NormalFidelity
66 | }
67 | }
68 |
69 | impl PolymeraseType {
70 | pub fn extension_time(&self, product_len: usize) -> u16 {
71 | match self {
72 | Self::NormalFidelity => (60 * product_len / 1_000) as u16,
73 | // 15 - 30. 15 recommended in FastCloning guide. PHusion manual: 15-30s/kb.
74 | Self::HighFidelity => (15 * product_len / 1_000) as u16, //
75 | }
76 | }
77 |
78 | pub fn denaturation(&self) -> TempTime {
79 | match self {
80 | Self::NormalFidelity => TempTime::new(94., 30),
81 | Self::HighFidelity => TempTime::new(98., 10), // pHusion manual: 5-10s.
82 | }
83 | }
84 |
85 | pub fn denaturation_initial(&self) -> TempTime {
86 | match self {
87 | Self::NormalFidelity => TempTime::new(94., 120),
88 | Self::HighFidelity => TempTime::new(98., 30),
89 | }
90 | }
91 |
92 | pub fn to_str(self) -> String {
93 | match self {
94 | Self::NormalFidelity => "Normal fidelity",
95 | Self::HighFidelity => "High fidelity (eg Phusion)",
96 | }
97 | .to_owned()
98 | }
99 | }
100 |
101 | #[derive(Default, Encode, Decode)]
102 | pub struct PcrParams {
103 | pub initial_denaturation: TempTime,
104 | pub denaturation: TempTime,
105 | pub annealing: TempTime,
106 | pub extension: TempTime,
107 | pub final_extension: TempTime,
108 | pub num_cycles: u16,
109 | }
110 |
111 | impl PcrParams {
112 | pub fn new(data: &PcrUi) -> Self {
113 | Self {
114 | initial_denaturation: data.polymerase_type.denaturation_initial(),
115 | denaturation: data.polymerase_type.denaturation(),
116 | // Alternative: Ta = 0.3 x Tm(primer) + 0.7 Tm(product) – 14.9.
117 | // 15-60s. How do we choose?. Phusion manual: 10-30s.
118 | annealing: TempTime::new(data.primer_tm - 5., 30),
119 | // 72 is good if Taq, and Phusion.
120 | extension: TempTime::new(72., data.polymerase_type.extension_time(data.product_len)),
121 | // Alternatively: 5-10 mins? Perhaps 30s per 1kb?) Phusion manual: 5-10m.
122 | final_extension: TempTime::new(72., 60 * 6),
123 | num_cycles: data.num_cycles,
124 | }
125 | }
126 | }
127 |
128 | /// Create a new tab containing of the PCR amplicon.
129 | pub fn make_amplicon_tab(
130 | state: &mut State,
131 | product_seq: Seq,
132 | range: RangeIncl,
133 | fwd_primer: Primer,
134 | rev_primer: Primer,
135 | ) {
136 | let mut product_features = Vec::new();
137 | for feature in &state.generic[state.active].features {
138 | // todo: Handle circle wraps etc.
139 | if range.start < feature.range.start && range.end > feature.range.end {
140 | let mut product_feature = feature.clone();
141 | // Find the indexes in the new product sequence.
142 | product_feature.range.start -= range.start - 1;
143 | product_feature.range.end -= range.start - 1;
144 |
145 | product_features.push(product_feature);
146 | }
147 | }
148 |
149 | state.add_tab();
150 | state.tabs_open.push(Default::default());
151 |
152 | // if let Some(seq) = range_combined.index_seq(&state.generic.seq) {
153 | state.generic[state.active].seq = product_seq;
154 |
155 | // Include the primers used for PCR, and features that are included in the new segment.
156 | // note that the feature indexes will need to change.
157 |
158 | let product_primers = vec![fwd_primer, rev_primer];
159 |
160 | state.generic[state.active].features = product_features;
161 | state.generic[state.active].primers = product_primers;
162 | state.generic[state.active].metadata.plasmid_name = "PCR amplicon".to_owned();
163 |
164 | state.sync_seq_related(None);
165 | // state.sync_primer_metrics();
166 |
167 | state.ui.page = Page::Sequence;
168 | state.ui.page_seq = PageSeq::View;
169 | }
170 |
--------------------------------------------------------------------------------
/src/portions.rs:
--------------------------------------------------------------------------------
1 | //! Code related to mixing portions. Used for performing quick mixing volume calculations.
2 |
3 | // todo: Consider including pKa info, and including tools to balance pH.
4 |
5 | use std::fmt::Display;
6 |
7 | use bincode::{Decode, Encode};
8 |
9 | #[derive(Clone, Encode, Decode)]
10 | pub struct PortionsState {
11 | pub solutions: Vec,
12 | pub media_input: MediaPrepInput,
13 | pub media_result: MediaPrep,
14 | }
15 |
16 | impl Default for PortionsState {
17 | fn default() -> Self {
18 | let media_input = MediaPrepInput::default();
19 | let media_result = media_prep(&media_input);
20 |
21 | let mut result = Self {
22 | solutions: Vec::new(),
23 | media_input,
24 | media_result,
25 | };
26 |
27 | result
28 | }
29 | }
30 |
31 | #[derive(Default, Clone, Encode, Decode)]
32 | pub struct Solution {
33 | pub name: String,
34 | /// Liters
35 | pub total_volume: f32,
36 | pub reagents: Vec,
37 | pub sub_solns: Vec,
38 | /// Volatile; not to be added to directly.
39 | pub reagents_sub_solns: Vec,
40 | }
41 |
42 | impl Solution {
43 | /// Find required amounts (mass or volume) for each reagent.
44 | pub fn calc_amounts(&mut self) {
45 | self.reagents_sub_solns = Vec::new();
46 | for sub_sol in &self.sub_solns {
47 | for reagent in &sub_sol.reagents {
48 | self.reagents_sub_solns.push(reagent.clone());
49 | }
50 | }
51 |
52 | for reagent in &mut self.reagents {
53 | reagent.calc_amount(self.total_volume);
54 | }
55 | }
56 | }
57 |
58 | #[derive(Clone, Copy, Encode, Decode)]
59 | pub enum AmountCalculated {
60 | /// grams
61 | Mass(f32),
62 | /// Liters
63 | Volume(f32),
64 | }
65 |
66 | impl Display for AmountCalculated {
67 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 | let str = match self {
69 | Self::Mass(v) => {
70 | if *v > 1. {
71 | format!("{:.2} g", v)
72 | } else if *v >= 0.001 {
73 | format!("{:.2} mg", v * 1_000.)
74 | } else {
75 | format!("{:.2} μg", v * 1_000_000.)
76 | }
77 | }
78 | // todo: Be careful about this calc being called frequently.
79 | Self::Volume(v) => {
80 | if *v > 1. {
81 | format!("{:.2} L", v)
82 | } else if *v >= 0.001 {
83 | format!("{:.2} mL", v * 1_000.)
84 | } else {
85 | // todo: If you have precision issues, make your base unit mL.
86 | format!("{:.2} μL", v * 1_000_000.)
87 | }
88 | }
89 | };
90 |
91 | write!(f, "{}", str)
92 | }
93 | }
94 |
95 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
96 | pub enum ReagentPrep {
97 | Mass,
98 | /// Inner: Molarity
99 | Volume(f32),
100 | }
101 |
102 | impl Display for ReagentPrep {
103 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 | let str = match self {
105 | Self::Mass => "Mass".to_owned(),
106 | // Self::Volume(molarity) => format!("Volume. Molarity: {molarity})"),
107 | Self::Volume(_molarity) => "Volume".to_owned(),
108 | };
109 | write!(f, "{}", str)
110 | }
111 | }
112 |
113 | #[derive(Clone, Encode, Decode)]
114 | pub struct Reagent {
115 | pub type_: ReagentType,
116 | pub prep: ReagentPrep,
117 | /// Target moles per liter in the solution
118 | pub molarity: f32,
119 | /// Calculated result
120 | pub amount_calc: AmountCalculated,
121 | }
122 |
123 | impl Default for Reagent {
124 | fn default() -> Self {
125 | Self {
126 | type_: ReagentType::Custom(0.),
127 | prep: ReagentPrep::Mass,
128 | molarity: 0.,
129 | amount_calc: AmountCalculated::Mass(0.),
130 | }
131 | }
132 | }
133 |
134 | impl Reagent {
135 | pub fn calc_amount(&mut self, total_volume: f32) {
136 | // mol = mol/L x L
137 | let moles_req = self.molarity * total_volume;
138 |
139 | self.amount_calc = match self.prep {
140 | // g = g/mol x mol
141 | ReagentPrep::Mass => AmountCalculated::Mass(self.type_.weight() * moles_req),
142 | // L = mol / mol/L:
143 | ReagentPrep::Volume(reagent_molarity) => {
144 | if reagent_molarity.abs() < 0.00000001 {
145 | AmountCalculated::Volume(0.)
146 | } else {
147 | AmountCalculated::Volume(moles_req / reagent_molarity)
148 | }
149 | }
150 | };
151 | }
152 | }
153 |
154 | /// A collection of common reagents, where we have molecular weights.
155 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
156 | pub enum ReagentType {
157 | Custom(f32), // Inner: Molecular weight
158 | /// Index of the solution in state.
159 | Solution(usize),
160 | SodiumChloride,
161 | SodiumPhosphateMonobasic,
162 | SodiumPhosphateDibasic,
163 | SodiumPhosphateDibasicHeptahydrate,
164 | PotassiumPhosphateMonobasic,
165 | PotassiumPhosphateDibasic,
166 | TrisHcl,
167 | Iptg,
168 | Imidazole,
169 | Lysozyme,
170 | Mes,
171 | Bes,
172 | Tes,
173 | CitricAcid,
174 | Edta,
175 | HydrochloricAcid,
176 | // AceticAcid,
177 | SodiumHydroxide,
178 | BromophenolBlue,
179 | Dtt,
180 | MagnesiumChloride,
181 | Glycine,
182 | Sds,
183 | Tris,
184 | }
185 |
186 | impl ReagentType {
187 | /// g/mol
188 | pub fn weight(&self) -> f32 {
189 | match self {
190 | Self::Custom(weight) => *weight,
191 | Self::Solution(_) => 0., // todo?
192 | Self::SodiumChloride => 58.44,
193 | Self::SodiumPhosphateMonobasic => 119.98,
194 | Self::SodiumPhosphateDibasic => 141.96,
195 | Self::SodiumPhosphateDibasicHeptahydrate => 268.10,
196 | Self::PotassiumPhosphateMonobasic => 136.08,
197 | Self::PotassiumPhosphateDibasic => 174.17,
198 | Self::TrisHcl => 121.14,
199 | Self::Iptg => 238.298,
200 | Self::Imidazole => 68.08,
201 | Self::Lysozyme => 14_388.,
202 | Self::Mes => 195.24,
203 | Self::Bes => 213.25,
204 | Self::Tes => 229.25,
205 | Self::CitricAcid => 192.12,
206 | Self::Edta => 292.24,
207 | Self::HydrochloricAcid => 36.46,
208 | Self::SodiumHydroxide => 40.,
209 | Self::BromophenolBlue => 669.96,
210 | Self::Dtt => 154.25,
211 | Self::MagnesiumChloride => 95.211,
212 | Self::Glycine => 75.07,
213 | Self::Sds => 288.5,
214 | Self::Tris => 121.14,
215 | }
216 | }
217 | }
218 |
219 | impl Display for ReagentType {
220 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 | let str = match self {
222 | Self::Custom(_) => "Custom".to_owned(),
223 | Self::Solution(_) => "Solution".to_owned(), // todo
224 | Self::SodiumChloride => "NaCl".to_owned(),
225 | Self::SodiumPhosphateMonobasic => "NaH₂PO₄ (Mono)".to_owned(),
226 | Self::SodiumPhosphateDibasic => "Na₂HPO₄ (Di)".to_owned(),
227 | Self::SodiumPhosphateDibasicHeptahydrate => "Na₂HPO₄·7H₂O".to_owned(),
228 | Self::PotassiumPhosphateMonobasic => "H₂KO₄P".to_owned(),
229 | Self::PotassiumPhosphateDibasic => "HK₂O₄P".to_owned(),
230 | Self::TrisHcl => "Tris-HCl".to_owned(),
231 | Self::Iptg => "IPTG".to_owned(),
232 | Self::Imidazole => "Imidazole".to_owned(),
233 | Self::Lysozyme => "Lysozyme".to_owned(),
234 | Self::Mes => "MES".to_owned(),
235 | Self::Bes => "BES".to_owned(),
236 | Self::Tes => "TES".to_owned(),
237 | Self::CitricAcid => "Citric acid".to_owned(),
238 | Self::Edta => "EDTA".to_owned(),
239 | Self::HydrochloricAcid => "HCl".to_owned(),
240 | Self::SodiumHydroxide => "NaOH".to_owned(),
241 | Self::BromophenolBlue => "Bromophenol blue".to_owned(),
242 | Self::Dtt => "DTT".to_owned(),
243 | Self::MagnesiumChloride => "MgCl₂".to_owned(),
244 | Self::Sds => "SDS".to_owned(),
245 | Self::Glycine => "Glycine".to_owned(),
246 | Self::Tris => "Tris".to_owned(),
247 | };
248 |
249 | write!(f, "{}", str)
250 | }
251 | }
252 |
253 | #[derive(Clone, Copy, PartialEq, Encode, Decode)]
254 | pub enum PlateSize {
255 | /// 60mm diameter
256 | D60,
257 | D90,
258 | D100,
259 | D150,
260 | }
261 |
262 | impl PlateSize {
263 | /// Nominal amount of liquid. Note that these go with the square of the baseline. We use 7mL for 60mm
264 | /// plates as the baseline.
265 | pub fn volume(&self) -> f32 {
266 | match self {
267 | Self::D60 => 0.007,
268 | Self::D90 => 0.01575,
269 | Self::D100 => 0.01944,
270 | Self::D150 => 0.04375,
271 | }
272 | }
273 | }
274 |
275 | impl Display for PlateSize {
276 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277 | let str = match self {
278 | Self::D60 => "60mm",
279 | Self::D90 => "90mm",
280 | Self::D100 => "100mm",
281 | Self::D150 => "150mm",
282 | };
283 |
284 | write!(f, "{}", str)
285 | }
286 | }
287 |
288 | #[derive(Clone, PartialEq, Encode, Decode)]
289 | pub enum MediaPrepInput {
290 | Plates((PlateSize, usize)), // number of plates,
291 | Liquid(f32), // volume
292 | }
293 |
294 | impl Display for MediaPrepInput {
295 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 | let str = match self {
297 | Self::Plates(_) => "Plates",
298 | Self::Liquid(_) => "Liquid culture",
299 | };
300 |
301 | write!(f, "{}", str)
302 | }
303 | }
304 |
305 | impl Default for MediaPrepInput {
306 | fn default() -> Self {
307 | Self::Plates((PlateSize::D90, 6))
308 | }
309 | }
310 |
311 | #[derive(Clone, Default, Encode, Decode, Debug)]
312 | pub struct MediaPrep {
313 | pub water: f32, // L
314 | pub food: f32, // g
315 | pub agar: f32, // g
316 | pub antibiotic: f32, // mL
317 | }
318 |
319 | /// Returns volume of water, grams of LB, grams of agar, mL of 1000x antibiotics.
320 | pub fn media_prep(input: &MediaPrepInput) -> MediaPrep {
321 | let (volume, agar) = match input {
322 | MediaPrepInput::Plates((plate_size, num)) => {
323 | let volume = plate_size.volume() * *num as f32;
324 | (volume, volume * 15.)
325 | }
326 | MediaPrepInput::Liquid(volume) => (*volume, 0.),
327 | };
328 |
329 | MediaPrep {
330 | water: volume,
331 | food: volume * 25.,
332 | agar,
333 | antibiotic: volume,
334 | }
335 | }
336 |
--------------------------------------------------------------------------------
/src/protein.rs:
--------------------------------------------------------------------------------
1 | //! Data related to proteins, eg derived from coding region features, in conjunction with
2 | //! reading frames.
3 |
4 | use bincode::{Decode, Encode};
5 | use bio_apis::rcsb::PdbData;
6 | use na_seq::{
7 | Nucleotide,
8 | amino_acids::{AminoAcid, CodingResult},
9 | };
10 |
11 | use crate::{
12 | misc_types::{Feature, FeatureType},
13 | reading_frame::{ReadingFrame, ReadingFrameMatch, find_orf_matches},
14 | state::State,
15 | };
16 |
17 | pub const WATER_WEIGHT: f32 = 18.015; // g/mol. We subtract these when calculating a protein's weight.
18 |
19 | // todo: Adjust A/R. Ideally, let the user customize it.
20 | pub const HYDROPATHY_WINDOW_SIZE: usize = 9;
21 |
22 | #[derive(Encode, Decode)]
23 | pub struct Protein {
24 | pub feature: Feature, // todo: Consider how you want to do this. Index? Only used for creation?
25 | pub reading_frame_match: ReadingFrameMatch,
26 | pub aa_seq: Vec,
27 | pub aa_seq_precoding: Vec,
28 | pub aa_seq_postcoding: Vec,
29 | pub weight: f32,
30 | pub weight_with_prepost: f32,
31 | // window center, value.
32 | /// This type is for storing graphable data used by egui_plot.
33 | // pub hydropath_data: Vec<[f64; 2]>,
34 | pub hydropath_data: Vec<(usize, f32)>,
35 | pub pdb_data: Vec,
36 | // Note: This is more of a UI functionality; here for now.
37 | pub show_hydropath: bool,
38 | }
39 |
40 | pub fn proteins_from_seq(
41 | seq: &[Nucleotide],
42 | features: &[Feature],
43 | cr_orf_matches: &[(usize, ReadingFrameMatch)],
44 | ) -> Vec {
45 | let mut result = Vec::new();
46 | for (i, feature) in features.iter().enumerate() {
47 | if feature.feature_type != FeatureType::CodingRegion {
48 | continue;
49 | }
50 | for (j, om) in cr_orf_matches {
51 | if *j == i {
52 | if let Some(seq_orf_match_dna) = om.range.index_seq(seq) {
53 | // todo: DRy with feature_db_load.
54 | let len = seq_orf_match_dna.len();
55 |
56 | let mut aa_seq = Vec::new();
57 | // We also render AAs included in the reading frame, but not specified in the coding region feature.
58 | let mut aa_seq_precoding = Vec::new();
59 | let mut aa_seq_postcoding = Vec::new();
60 |
61 | for i_ in 0..len / 3 {
62 | let i = i_ * 3; // The ORF-modified sequence index.
63 | let i_actual = i + om.range.start;
64 |
65 | let nts = &seq_orf_match_dna[i..i + 3];
66 |
67 | // Note: We are ignoring stop codons here.
68 | if let CodingResult::AminoAcid(aa) =
69 | AminoAcid::from_codons(nts.try_into().unwrap())
70 | {
71 | if i_actual < feature.range.start {
72 | aa_seq_precoding.push(aa);
73 | } else if i_actual > feature.range.end {
74 | aa_seq_postcoding.push(aa);
75 | } else {
76 | aa_seq.push(aa);
77 | }
78 | }
79 | }
80 |
81 | // todo: Def cache these weights.
82 | let weight = protein_weight(&aa_seq);
83 | let weight_with_prepost = weight
84 | + protein_weight(&aa_seq_precoding)
85 | + protein_weight(&aa_seq_postcoding);
86 |
87 | let hydropath_data = hydropathy_doolittle(&aa_seq, HYDROPATHY_WINDOW_SIZE);
88 | // Convert to a format accepted by our plotting lib.
89 | // let mut hydropath_data = Vec::new();
90 | // for (i, val) in hydropath_data_ {
91 | // hydropath_data.push([i as f64, val as f64]);
92 | // }
93 |
94 | result.push(Protein {
95 | feature: feature.clone(),
96 | reading_frame_match: om.clone(),
97 | aa_seq,
98 | aa_seq_precoding,
99 | aa_seq_postcoding,
100 | hydropath_data,
101 | weight,
102 | weight_with_prepost,
103 | show_hydropath: true,
104 | pdb_data: Vec::new(),
105 | })
106 | }
107 | }
108 | }
109 | }
110 | result
111 | }
112 |
113 | /// Calculates the Kyte-Doolittle value of Hydropathy index. Uses per-AA
114 | /// experimentally-determined hydrophobicity values, averaged over a moving window.
115 | ///
116 | /// https://web.expasy.org/protscale/
117 | pub fn hydropathy_doolittle(seq: &[AminoAcid], window_size: usize) -> Vec<(usize, f32)> {
118 | if window_size % 2 == 0 {
119 | eprintln!("Window size for KD must be odd");
120 | return Vec::new();
121 | }
122 | let mut result = Vec::new();
123 |
124 | let win_div_2 = window_size / 2; // Rounds down.
125 |
126 | if win_div_2 - 1 >= seq.len() {
127 | eprintln!("Error with window size for hydropathy");
128 | return result;
129 | }
130 |
131 | // We'll center each window on `i`.
132 | for i in win_div_2..seq.len() - win_div_2 - 1 {
133 | let aas = &seq[i - win_div_2..i + win_div_2 + 1];
134 | let mut val_this_window = 0.;
135 | for aa in aas {
136 | val_this_window += aa.hydropathicity();
137 | }
138 | result.push((i, val_this_window / window_size as f32));
139 | }
140 |
141 | result
142 | }
143 |
144 | /// The TANGO algorithm predicts beta-sheet formation propensity, which is associated with aggregation.
145 | /// It considers factors like amino acid sequence, solvent accessibility, and secondary structure.
146 | pub fn tango_aggregation(seq: &[AminoAcid]) {}
147 |
148 | /// Predicts aggregation-prone regionis by calculating an aggregation score for each AA.
149 | /// todo: Look this up.
150 | pub fn aggrescan(seq: &[AminoAcid]) {}
151 |
152 | // todo: Look up SLIDER aggregation tool as well.
153 |
154 | // todo: Visualize hydrophobic and aggreg-prone regions.
155 |
156 | // todo: Move this somewhere more appropriate, then store in state_volatile.
157 | /// Find the most likely reading frame for each coding region.
158 | pub fn sync_cr_orf_matches(state: &mut State) {
159 | state.volatile[state.active].cr_orf_matches = Vec::new();
160 |
161 | // These matches are for all frames.
162 | let mut region_matches = Vec::new();
163 |
164 | for orf in [
165 | ReadingFrame::Fwd0,
166 | ReadingFrame::Fwd1,
167 | ReadingFrame::Fwd2,
168 | ReadingFrame::Rev0,
169 | ReadingFrame::Rev1,
170 | ReadingFrame::Rev2,
171 | ] {
172 | let mut regions = find_orf_matches(state.get_seq(), orf);
173 | region_matches.append(&mut regions);
174 | }
175 |
176 | for (i, feature) in state.generic[state.active].features.iter().enumerate() {
177 | if feature.feature_type != FeatureType::CodingRegion {
178 | continue;
179 | }
180 |
181 | // Don't show binding tags.
182 | let label_lower = feature.label.to_lowercase().replace(' ', "");
183 | if label_lower.contains("xhis") || label_lower.contains("×his") {
184 | continue;
185 | }
186 |
187 | // Find the best reading frame match, if there is one.
188 | let mut orf_match = None;
189 | let mut smallest_match = usize::MAX;
190 | for rm in ®ion_matches {
191 | if !rm.range.contains(feature.range.start) || !rm.range.contains(feature.range.end) {
192 | continue;
193 | }
194 |
195 | // If multiple matches contain the range, choose the smallest one.
196 | if rm.range.len() < smallest_match {
197 | smallest_match = rm.range.len();
198 | orf_match = Some(rm.clone());
199 | }
200 | }
201 |
202 | if let Some(m) = orf_match {
203 | state.volatile[state.active]
204 | .cr_orf_matches
205 | .push((i, m.clone()));
206 | }
207 | }
208 | }
209 |
210 | /// calculate protein weight in kDa.
211 | pub fn protein_weight(seq: &[AminoAcid]) -> f32 {
212 | if seq.is_empty() {
213 | return 0.; // Avoids underflow.
214 | }
215 | let mut result = 0.;
216 | for aa in seq {
217 | result += aa.weight();
218 | }
219 | (result - WATER_WEIGHT * (seq.len() - 1) as f32) / 1_000.
220 | }
221 |
--------------------------------------------------------------------------------
/src/reading_frame.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 |
3 | use bincode::{Decode, Encode};
4 | use na_seq::{
5 | Nucleotide,
6 | Nucleotide::{A, G, T},
7 | Seq, seq_complement,
8 | };
9 |
10 | use crate::util::RangeIncl;
11 |
12 | const START_CODON: [Nucleotide; 3] = [A, T, G];
13 | pub const STOP_CODONS: [[Nucleotide; 3]; 3] = [[T, A, A], [T, A, G], [T, G, A]];
14 |
15 | /// Of the 6 possible reading frames.
16 | #[derive(Clone, Copy, PartialEq, Debug, Encode, Decode)]
17 | pub enum ReadingFrame {
18 | /// Forward, with 0 offset (This pattern applies for all variants)
19 | Fwd0,
20 | Fwd1,
21 | Fwd2,
22 | Rev0,
23 | Rev1,
24 | Rev2,
25 | }
26 |
27 | impl ReadingFrame {
28 | pub fn offset(&self) -> usize {
29 | match self {
30 | Self::Fwd0 | Self::Rev0 => 0,
31 | Self::Fwd1 | Self::Rev1 => 1,
32 | Self::Fwd2 | Self::Rev2 => 2,
33 | }
34 | }
35 |
36 | pub fn is_reverse(&self) -> bool {
37 | // todo: Enum
38 | !matches!(self, Self::Fwd0 | Self::Fwd1 | Self::Fwd2)
39 | }
40 |
41 | /// Get a seqeuence of the full sequence in the appropriate direction, and with the appropriate offset.
42 | /// The result will always start in frame. It will be trimmed if offset > 0.
43 | /// todo: THis makes a clone. Can we instead do a slice?
44 | pub fn arrange_seq(&self, seq: &[Nucleotide]) -> Seq {
45 | let offset = self.offset();
46 |
47 | if self.is_reverse() {
48 | seq_complement(seq)[offset..].to_vec()
49 | } else {
50 | seq[offset..].to_vec()
51 | }
52 | }
53 | }
54 |
55 | impl Default for ReadingFrame {
56 | fn default() -> Self {
57 | Self::Fwd0
58 | }
59 | }
60 |
61 | impl Display for ReadingFrame {
62 | /// For use with selector buttons.
63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 | let str = match self {
65 | Self::Fwd0 => "Fwd 0",
66 | Self::Fwd1 => "Fwd 1",
67 | Self::Fwd2 => "Fwd 2",
68 | Self::Rev0 => "Rev 0",
69 | Self::Rev1 => "Rev 1",
70 | Self::Rev2 => "Rev 2",
71 | }
72 | .to_owned();
73 | write!(f, "{}", str)
74 | }
75 | }
76 |
77 | #[derive(Debug, Clone, Encode, Decode)]
78 | pub struct ReadingFrameMatch {
79 | pub frame: ReadingFrame,
80 | /// Indices are respective to the non-complementary seq, for both forward and reverse reading frames.
81 | pub range: RangeIncl,
82 | }
83 |
84 | /// Find coding regions in a sequence, given a reading frame.
85 | pub fn find_orf_matches(seq: &[Nucleotide], orf: ReadingFrame) -> Vec {
86 | let mut result = Vec::new();
87 |
88 | let offset = orf.offset();
89 | let seq_len_full = seq.len();
90 |
91 | if seq_len_full < 3 {
92 | return result;
93 | }
94 |
95 | let seq_ = orf.arrange_seq(&seq);
96 | let len = seq_.len();
97 |
98 | let mut frame_open = None; // Inner: Start index.
99 |
100 | for i_ in 0..len / 3 {
101 | let i = i_ * 3; // The actual sequence index.
102 |
103 | let nts = &seq_[i..i + 3];
104 |
105 | if frame_open.is_none() && nts == START_CODON {
106 | frame_open = Some(i);
107 | // } else if frame_open.is_some() && stop_codons.contains(nts.try_into().unwrap()) {
108 | } else if frame_open.is_some()
109 | && (STOP_CODONS.contains(nts.try_into().unwrap()) || seq_len_full - i <= 3)
110 | {
111 | // If we reach the end of the sequence, consider it closed.
112 | // todo: Handle circular around the origin. Ie, don't auto-close in that case.
113 | // + 1 for our 1-based seq name convention.
114 | // This section's a bit hairy; worked by trial and error. Final indices are respective to
115 | // the non-complementary seq, for both forward and reverse reading frames.
116 | let range = if !orf.is_reverse() {
117 | // todo: We still have wonkiness here.
118 | // RangeIncl::new(frame_open.unwrap() + 1 + offset, i + 2 + offset)
119 | RangeIncl::new(frame_open.unwrap() + 1 + offset, i + 3 + offset)
120 | } else {
121 | RangeIncl::new(
122 | seq_len_full - (i + 2 + offset),
123 | seq_len_full - (frame_open.unwrap() + offset) - 1,
124 | )
125 | };
126 |
127 | result.push(ReadingFrameMatch { frame: orf, range });
128 | frame_open = None;
129 | }
130 | }
131 |
132 | result
133 | }
134 |
--------------------------------------------------------------------------------
/src/resources/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/src/resources/icon.ico
--------------------------------------------------------------------------------
/src/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/David-OConnor/plascad/21681308368918f9d1ad51517c47de3f990915a5/src/resources/icon.png
--------------------------------------------------------------------------------
/src/save_compat.rs:
--------------------------------------------------------------------------------
1 | //! This module contains archived state structs used to open saves from previous versions
2 | //! of this program, and convert them to the latest version.
3 |
--------------------------------------------------------------------------------
/src/solution_helper.rs:
--------------------------------------------------------------------------------
1 | //! This module contains code for assisting with mixing common solutions
2 |
--------------------------------------------------------------------------------
/src/tags.rs:
--------------------------------------------------------------------------------
1 | //! This module contains info related to tags, like the 6x HIS tag.
2 |
3 | use std::ops::RangeInclusive;
4 |
5 | use na_seq::Nucleotide;
6 |
7 | pub struct TagMatch {
8 | pub lib_index: usize,
9 | // todo: Experimenting with ranges vice (usize, usize)
10 | /// 0-based indexing.
11 | pub seq: RangeInclusive,
12 | }
13 |
14 | pub struct Tag {
15 | pub name: String,
16 | // From the 5' end.
17 | // pub seq: Seq, // todo: You may eventually need Vec.
18 | }
19 |
20 | impl Tag {
21 | pub fn new(name: &str) -> Self {
22 | Self {
23 | name: name.to_owned(),
24 | }
25 | }
26 | }
27 |
28 | // T7 promoter: TAATACGACTCACTATAG
29 | // T7 term: GCTAGTTATTGCTCAGCGG
30 | // T7 term take 2: ctagcataaccccttggggcctctaaacgggtcttgaggggttttttg
31 |
32 | pub fn _find_tag_matches(seq: &[Nucleotide], lib: &[Tag]) -> Vec {
33 | let mut result = Vec::new();
34 |
35 | result
36 | }
37 |
38 | /// Load a set of common Restriction enzymes. Call this at program start, to load into a state field.
39 | pub fn _load_tag_library() -> Vec {
40 | vec![
41 | // todo: Hisx6+x,
42 | // todo: T7 promoter
43 | // Tag::new("AatII", vec![G, A, C, G, T, C], 4),
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/src/toxic_proteins.rs:
--------------------------------------------------------------------------------
1 | //! Contains code related to identifying toxic proteins.
2 |
3 | use na_seq::{
4 | Nucleotide,
5 | amino_acids::{AminoAcid, CodingResult},
6 | };
7 |
8 | use crate::protein::proteins_from_seq;
9 |
10 | const GLN_TRACT_SCAN_FRAME: usize = 35;
11 | const GLN_MAX_PORTION: f32 = 0.75;
12 |
13 | #[derive(Clone, Copy, PartialEq)]
14 | enum ToxicStatus {
15 | Pass,
16 | Fail,
17 | }
18 |
19 | // #[derive(Clone, Copy)]
20 | // enum Host {
21 | // Ecoli,
22 | // Aav,
23 | // }
24 |
25 | /// Identify tracts, in all reading frames, of long sequences of Glutamine; this may lead to aggregation,
26 | /// and therefor toxicity. Sequences of 35 Gln in a row are a good indicator of this.
27 | fn check_glu_tracts(seq: &[Nucleotide]) -> ToxicStatus {
28 | let len = seq.len();
29 |
30 | let mut seq_rev = seq.to_vec();
31 | seq_rev.reverse();
32 |
33 | // For each reading frame, create a Vec of amino acids.
34 | for seq_ in &[seq, &seq_rev] {
35 | for rf_offset in 0..2 {
36 | let mut aa_seq = Vec::new();
37 |
38 | for i_ in 0..len / 3 {
39 | let i = i_ * 3 + rf_offset; // The ORF-modified sequence index.
40 |
41 | if i + 3 >= len {
42 | break;
43 | }
44 |
45 | let nts = &seq_[i..i + 3];
46 |
47 | // let mut matched = false;
48 | if let CodingResult::AminoAcid(aa) = AminoAcid::from_codons(nts.try_into().unwrap())
49 | {
50 | // Note: We are ignoring stop codons here.
51 | aa_seq.push(aa)
52 | }
53 | }
54 |
55 | // Now that we have a set of AA for this reading frame, scan for Glu tracts.
56 | // If we find any, return the failure state.
57 |
58 | for start_i in 0..aa_seq.len() - GLN_TRACT_SCAN_FRAME {
59 | let mut num_gln = 0;
60 | for i in start_i..start_i + aa_seq.len() {
61 | if aa_seq[i] == AminoAcid::Gln {
62 | num_gln += 1;
63 | }
64 | }
65 |
66 | if num_gln as f32 / GLN_TRACT_SCAN_FRAME as f32 > GLN_MAX_PORTION {
67 | return ToxicStatus::Fail;
68 | }
69 | }
70 | }
71 | }
72 |
73 | ToxicStatus::Pass
74 | }
75 |
--------------------------------------------------------------------------------