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