1mod svg;
5
6use std::ops::Range;
7
8use termal_alignment::{rgb::ResidueColorMap, Alignment};
9
10pub use svg::export_svg;
11
12#[derive(Clone, Debug)]
13pub struct Region {
14 pub rows: Range<usize>,
15 pub cols: Range<usize>,
16}
17
18const GUTTER_WIDTH: f32 = 20.0; #[derive(Clone, Debug)]
21pub struct ExportOpts {
22 pub region: Region,
23 pub cell_width: f32,
24 pub cell_height: f32,
25 pub residue_font_size: u32,
26 pub ascent_corr: f32,
27 pub char_width: f32,
28 pub colormap: ResidueColorMap,
29 pub margin_x: f32,
30 pub margin_y: f32,
31 pub cell_frames: bool,
32 pub hdr_pane_width_corr: f32,
33}
34
35#[derive(Clone, Debug)]
36pub struct Layout {
37 pub grid_width: f32,
38 pub grid_height: f32,
39 pub hdr_txt_width: f32,
40}
41
42pub fn compute_layout(aln: &Alignment, opts: &ExportOpts) -> Layout {
43 let max_hdr_len = aln.headers
44 .iter()
45 .skip(opts.region.rows.start)
46 .take(opts.region.rows.end - opts.region.rows.start)
47 .map(|h| h.len()).max().unwrap_or(0);
48 let hdr_txt_width = max_hdr_len as f32 * opts.char_width * opts.hdr_pane_width_corr + GUTTER_WIDTH;
49
50 Layout {
51 grid_width: hdr_txt_width + (opts.region.cols.end - opts.region.cols.start) as f32 * opts.cell_width,
52 grid_height: (opts.region.rows.end - opts.region.rows.start) as f32 * opts.cell_height,
53 hdr_txt_width,
54 }
55}
56
57impl Default for ExportOpts {
60 fn default() -> Self {
61 let colormap: ResidueColorMap = ResidueColorMap::aa_lesk();
62 Self {
63 region: Region { rows: (0..10), cols: (0..20) },
64 cell_width: 11.0,
65 cell_height: 12.0,
66 residue_font_size: 14,
67 ascent_corr: 12.0,
68 char_width: 8.0,
69 colormap,
70 margin_x: 10.0,
71 margin_y: 10.0,
72 cell_frames: false,
73 hdr_pane_width_corr: 1.1,
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::{compute_layout, export_svg, ExportOpts};
81 use termal_alignment::Alignment;
82 use crate::Region;
83
84 #[test]
85 fn compute_layout_uses_longest_header_and_alignment_size() {
86 let aln = Alignment::from_vecs(
87 vec!["a".to_string(), "long_hdr".to_string()],
88 vec!["ACG".to_string(), "TTA".to_string()],
89 );
90
91 let mut opts = ExportOpts::default();
92 opts.region = Region {
93 rows: 0..aln.num_seq(),
94 cols: 0..aln.aln_len(),
95 };
96
97 let layout = compute_layout(&aln, &opts);
98 assert_eq!(layout.hdr_txt_width, 90.4);
99 assert_eq!(layout.grid_width, 123.4);
100 assert_eq!(layout.grid_height, 24.0);
101 }
102
103 fn test_alignment() -> Alignment {
104 Alignment::from_vecs(
105 vec!["seq0".to_string(), "seq1".to_string(), "seq2".to_string(), "seq3".to_string()],
106 vec!["ACGTACGTACGT".to_string(), "TGCATGCATGCA".to_string(),
107 "GGGGGGGGGGGG".to_string(), "TTTTTTTTTTTT".to_string()],
108 )
109 }
110
111 fn count_svg_rows(svg: &str) -> usize {
112 svg.matches("<g transform='translate(0,").count()
113 }
114
115 fn count_svg_cols(svg: &str) -> usize {
116 svg.lines().find(|l| l.contains("<rect"))
117 .map(|l| l.matches("<rect").count())
118 .unwrap_or(0)
119 }
120
121 #[test]
122 fn region_full_alignment_exports_all_rows_and_cols() {
123 let aln = test_alignment();
124 let mut opts = ExportOpts::default();
125 opts.region = Region {
126 rows: 0..aln.num_seq(),
127 cols: 0..aln.aln_len(),
128 };
129
130 let layout = compute_layout(&aln, &opts);
131 let mut out = Vec::new();
132 export_svg(&aln, &opts, &layout, &mut out).unwrap();
133 let svg = String::from_utf8(out).unwrap();
134
135 assert_eq!(count_svg_rows(&svg), 4, "should export all 4 sequences");
136 assert_eq!(count_svg_cols(&svg), 12, "should export all 12 columns");
137 }
138
139 #[test]
140 fn region_row_subset_exports_only_specified_rows() {
141 let aln = test_alignment();
142 let mut opts = ExportOpts::default();
143 opts.region = Region {
144 rows: 1..3, cols: 0..aln.aln_len(),
146 };
147
148 let layout = compute_layout(&aln, &opts);
149 let mut out = Vec::new();
150 export_svg(&aln, &opts, &layout, &mut out).unwrap();
151 let svg = String::from_utf8(out).unwrap();
152
153 assert_eq!(count_svg_rows(&svg), 2, "should export 2 rows (1:3)");
154 assert_eq!(count_svg_cols(&svg), 12, "should export all 12 columns");
155 assert!(svg.contains("seq1"), "should contain seq1 header");
156 assert!(svg.contains("seq2"), "should contain seq2 header");
157 assert!(!svg.contains("seq0"), "should not contain seq0");
158 assert!(!svg.contains("seq3"), "should not contain seq3");
159 }
160
161 #[test]
162 fn region_col_subset_exports_only_specified_cols() {
163 let aln = test_alignment();
164 let mut opts = ExportOpts::default();
165 opts.region = Region {
166 rows: 0..aln.num_seq(),
167 cols: 3..9, };
169
170 let layout = compute_layout(&aln, &opts);
171 let mut out = Vec::new();
172 export_svg(&aln, &opts, &layout, &mut out).unwrap();
173 let svg = String::from_utf8(out).unwrap();
174
175 assert_eq!(count_svg_rows(&svg), 4, "should export all 4 sequences");
176 assert_eq!(count_svg_cols(&svg), 6, "should export 6 columns (3:9)");
177 }
178
179 #[test]
180 fn region_row_and_col_subset_exports_specified_rect() {
181 let aln = test_alignment();
182 let mut opts = ExportOpts::default();
183 opts.region = Region {
184 rows: 1..3, cols: 2..6, };
187
188 let layout = compute_layout(&aln, &opts);
189 let mut out = Vec::new();
190 export_svg(&aln, &opts, &layout, &mut out).unwrap();
191 let svg = String::from_utf8(out).unwrap();
192
193 assert_eq!(count_svg_rows(&svg), 2, "should export 2 rows (1:3)");
194 assert_eq!(count_svg_cols(&svg), 4, "should export 4 columns (2:6)");
195 assert!(svg.contains("seq1"), "should contain seq1");
196 assert!(svg.contains("seq2"), "should contain seq2");
197 }
198}