1use std::fmt::{Display, Formatter};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct Rgb {
8 pub r: u8,
9 pub g: u8,
10 pub b: u8,
11}
12
13impl Rgb {
14 pub const fn from_u32(h: u32) -> Self {
18 Self {
20 r: ((h >> 16) & 0xFF) as u8,
21 g: ((h >> 8) & 0xFF) as u8,
22 b: (h & 0xFF) as u8,
23 }
24 }
25
26 pub fn from_hex(s: &str) -> Result<Self, &'static str> {
29 let s = s.trim();
30
31 let s = s
33 .strip_prefix("0x")
34 .or_else(|| s.strip_prefix("#"))
35 .unwrap_or(s);
36
37 match s.len() {
38 6 => {
39 let value = u32::from_str_radix(s, 16).map_err(|_| "invalid hex color")?;
41
42 Ok(Self {
43 r: ((value >> 16) & 0xFF) as u8,
44 g: ((value >> 8) & 0xFF) as u8,
45 b: (value & 0xFF) as u8,
46 })
47 }
48 8 => {
49 let value = u32::from_str_radix(s, 16).map_err(|_| "invalid hex color")?;
51
52 Ok(Self {
53 r: ((value >> 16) & 0xFF) as u8,
54 g: ((value >> 8) & 0xFF) as u8,
55 b: (value & 0xFF) as u8,
56 })
57 }
58 _ => Err("hex color must have 6 or 8 digits"),
59 }
60 }
61
62 pub fn to_hex(self) -> String {
64 format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
65 }
66}
67
68pub const RGB_RED: Rgb = Rgb { r: 255, g: 0, b: 0 };
69pub const RGB_GRAY: Rgb = Rgb {
70 r: 127,
71 g: 127,
72 b: 127,
73};
74pub const RGB_LIGHT_GRAY: Rgb = Rgb {
75 r: 224,
76 g: 224,
77 b: 224,
78};
79pub const RGB_WHITE: Rgb = Rgb {
80 r: 255,
81 g: 255,
82 b: 255,
83};
84
85pub const GAP_COLOR: Rgb = RGB_LIGHT_GRAY;
86
87pub const TERMAL_ORANGE: Rgb = Rgb {
89 r: 255,
90 g: 165,
91 b: 0,
92};
93pub const TERMAL_SALMON: Rgb = Rgb {
94 r: 250,
95 g: 128,
96 b: 114,
97};
98
99pub const ASCII_8_COLOR_GREEN: Rgb = Rgb { r: 0, g: 128, b: 0 };
101pub const ASCII_8_COLOR_MAGENTA: Rgb = Rgb {
102 r: 128,
103 g: 0,
104 b: 128,
105};
106pub const ASCII_8_COLOR_RED: Rgb = Rgb { r: 128, g: 0, b: 0 };
107pub const ASCII_8_COLOR_BLUE: Rgb = Rgb { r: 0, g: 0, b: 128 };
108
109pub const LESK_ORANGE: Rgb = TERMAL_ORANGE;
111pub const LESK_GREEN: Rgb = ASCII_8_COLOR_GREEN;
112pub const LESK_BLUE: Rgb = ASCII_8_COLOR_BLUE;
113pub const LESK_MAGENTA: Rgb = ASCII_8_COLOR_MAGENTA;
114pub const LESK_RED: Rgb = ASCII_8_COLOR_RED;
115
116pub const CLUSTALX_RED: Rgb = Rgb {
119 r: 229,
120 g: 51,
121 b: 25,
122};
123pub const CLUSTALX_BLUE: Rgb = Rgb {
124 r: 25,
125 g: 127,
126 b: 229,
127};
128pub const CLUSTALX_GREEN: Rgb = Rgb {
129 r: 25,
130 g: 204,
131 b: 25,
132};
133pub const CLUSTALX_CYAN: Rgb = Rgb {
134 r: 25,
135 g: 178,
136 b: 178,
137};
138pub const CLUSTALX_PINK: Rgb = Rgb {
139 r: 229,
140 g: 127,
141 b: 127,
142};
143pub const CLUSTALX_MAGENTA: Rgb = Rgb {
144 r: 204,
145 g: 76,
146 b: 204,
147};
148pub const CLUSTALX_YELLOW: Rgb = Rgb {
149 r: 204,
150 g: 204,
151 b: 0,
152};
153pub const CLUSTALX_ORANGE: Rgb = Rgb {
154 r: 229,
155 g: 153,
156 b: 76,
157};
158
159pub const JALVIEW_NUCLEOTIDE_A: Rgb = Rgb::from_u32(0x0064F73F);
163pub const JALVIEW_NUCLEOTIDE_C: Rgb = Rgb::from_u32(0x00FFB340);
164pub const JALVIEW_NUCLEOTIDE_G: Rgb = Rgb::from_u32(0x00EB413C);
165pub const JALVIEW_NUCLEOTIDE_T: Rgb = Rgb::from_u32(0x003C88EE);
166pub const JALVIEW_NUCLEOTIDE_U: Rgb = Rgb::from_u32(0x003C88EE);
167pub const JALVIEW_NUCLEOTIDE_I: Rgb = Rgb::from_u32(0x00ffffff);
168pub const JALVIEW_NUCLEOTIDE_X: Rgb = Rgb::from_u32(0x004f6f6f);
169pub const JALVIEW_NUCLEOTIDE_R: Rgb = Rgb::from_u32(0x00CD5C5C);
170pub const JALVIEW_NUCLEOTIDE_Y: Rgb = Rgb::from_u32(0x00008000);
171pub const JALVIEW_NUCLEOTIDE_W: Rgb = Rgb::from_u32(0x004682B4);
172pub const JALVIEW_NUCLEOTIDE_S: Rgb = Rgb::from_u32(0x00FF8C00);
173pub const JALVIEW_NUCLEOTIDE_M: Rgb = Rgb::from_u32(0x009ACD32);
174pub const JALVIEW_NUCLEOTIDE_K: Rgb = Rgb::from_u32(0x009932CC);
175pub const JALVIEW_NUCLEOTIDE_B: Rgb = Rgb::from_u32(0x008b4513);
176pub const JALVIEW_NUCLEOTIDE_H: Rgb = Rgb::from_u32(0x00808080);
177pub const JALVIEW_NUCLEOTIDE_D: Rgb = Rgb::from_u32(0x00483D8B);
178pub const JALVIEW_NUCLEOTIDE_V: Rgb = Rgb::from_u32(0x00b8860b);
179pub const JALVIEW_NUCLEOTIDE_N: Rgb = Rgb::from_u32(0x002f4f4f);
180
181#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum ColorMapName {
183 AALesk,
184 AAClustalX,
185 DNAJalView,
186 Monochrome,
187}
188
189#[derive(Clone, Debug)]
190pub struct ResidueColorMap {
191 table: [Rgb; 256],
192}
193
194impl ResidueColorMap {
195 pub fn with_default(gap_color: Rgb, default: Rgb) -> Self {
196 let mut tbl = [default; 256];
197 tbl[b'-' as usize] = gap_color;
198 tbl[b'.' as usize] = gap_color;
199 Self { table: tbl }
200 }
201
202 pub fn by_name(name: ColorMapName) -> Self {
203 match name {
204 ColorMapName::AALesk => Self::aa_lesk(),
205 ColorMapName::AAClustalX => Self::aa_clustalx(),
206 ColorMapName::DNAJalView => Self::dna_jalview(),
207 ColorMapName::Monochrome => Self::monochrome(),
208 }
209 }
210
211 #[inline]
212 pub fn rgb(&self, b: u8) -> Rgb {
213 self.table[b as usize]
214 }
215
216 fn set(&mut self, b: u8, color: Rgb) {
217 self.table[b as usize] = color;
218 }
219
220 pub fn set_pair(&mut self, b: u8, color: Rgb) {
221 self.set(b.to_ascii_lowercase(), color);
222 self.set(b.to_ascii_uppercase(), color);
223 }
224
225 pub fn monochrome() -> Self {
226 Self::with_default(GAP_COLOR, RGB_WHITE)
227 }
228
229 pub fn dna_basic() -> Self {
230 let mut map = Self::with_default(
231 GAP_COLOR,
232 Rgb {
233 r: 160,
234 g: 160,
235 b: 160,
236 },
237 );
238 map.set_pair(b'A', Rgb { r: 0, g: 200, b: 0 });
239 map.set_pair(b'T', Rgb { r: 200, g: 0, b: 0 });
240 map.set_pair(
241 b'G',
242 Rgb {
243 r: 255,
244 g: 165,
245 b: 0,
246 },
247 );
248 map.set_pair(b'C', Rgb { r: 0, g: 0, b: 200 });
249 map
250 }
251
252 pub fn dna_jalview() -> Self {
253 let mut map = Self::with_default(GAP_COLOR, RGB_WHITE);
254 map.set_pair(b'A', JALVIEW_NUCLEOTIDE_A);
255 map.set_pair(b'C', JALVIEW_NUCLEOTIDE_C);
256 map.set_pair(b'G', JALVIEW_NUCLEOTIDE_G);
257 map.set_pair(b'T', JALVIEW_NUCLEOTIDE_T);
258 map.set_pair(b'U', JALVIEW_NUCLEOTIDE_U);
259 map.set_pair(b'I', JALVIEW_NUCLEOTIDE_I);
260 map.set_pair(b'X', JALVIEW_NUCLEOTIDE_X);
261 map.set_pair(b'R', JALVIEW_NUCLEOTIDE_R);
262 map.set_pair(b'Y', JALVIEW_NUCLEOTIDE_Y);
263 map.set_pair(b'W', JALVIEW_NUCLEOTIDE_W);
264 map.set_pair(b'S', JALVIEW_NUCLEOTIDE_S);
265 map.set_pair(b'M', JALVIEW_NUCLEOTIDE_M);
266 map.set_pair(b'K', JALVIEW_NUCLEOTIDE_K);
267 map.set_pair(b'B', JALVIEW_NUCLEOTIDE_B);
268 map.set_pair(b'H', JALVIEW_NUCLEOTIDE_H);
269 map.set_pair(b'D', JALVIEW_NUCLEOTIDE_D);
270 map.set_pair(b'V', JALVIEW_NUCLEOTIDE_V);
271 map.set_pair(b'N', JALVIEW_NUCLEOTIDE_N);
272 map
273 }
274
275 pub fn aa_clustalx() -> Self {
276 let mut map = Self::with_default(GAP_COLOR, RGB_WHITE);
277 map.set_pair(b'G', CLUSTALX_ORANGE);
278 map.set_pair(b'A', CLUSTALX_BLUE);
279 map.set_pair(b'S', CLUSTALX_GREEN);
280 map.set_pair(b'T', CLUSTALX_GREEN);
281 map.set_pair(b'C', CLUSTALX_PINK);
282 map.set_pair(b'V', CLUSTALX_BLUE);
283 map.set_pair(b'I', CLUSTALX_BLUE);
284 map.set_pair(b'L', CLUSTALX_BLUE);
285 map.set_pair(b'P', CLUSTALX_YELLOW);
286 map.set_pair(b'F', CLUSTALX_BLUE);
287 map.set_pair(b'Y', CLUSTALX_CYAN);
288 map.set_pair(b'M', CLUSTALX_BLUE);
289 map.set_pair(b'W', CLUSTALX_BLUE);
290 map.set_pair(b'N', CLUSTALX_GREEN);
291 map.set_pair(b'Q', CLUSTALX_GREEN);
292 map.set_pair(b'H', CLUSTALX_CYAN);
293 map.set_pair(b'D', CLUSTALX_MAGENTA);
294 map.set_pair(b'E', CLUSTALX_MAGENTA);
295 map.set_pair(b'K', CLUSTALX_RED);
296 map.set_pair(b'R', CLUSTALX_RED);
297 map
298 }
299
300 pub fn aa_lesk() -> Self {
301 let mut map = Self::with_default(GAP_COLOR, RGB_WHITE);
302 map.set_pair(b'G', TERMAL_ORANGE);
303 map.set_pair(b'A', TERMAL_ORANGE);
304 map.set_pair(b'S', TERMAL_ORANGE);
305 map.set_pair(b'T', TERMAL_ORANGE);
306 map.set_pair(b'C', ASCII_8_COLOR_GREEN);
307 map.set_pair(b'V', ASCII_8_COLOR_GREEN);
308 map.set_pair(b'I', ASCII_8_COLOR_GREEN);
309 map.set_pair(b'L', ASCII_8_COLOR_GREEN);
310 map.set_pair(b'P', ASCII_8_COLOR_GREEN);
311 map.set_pair(b'F', ASCII_8_COLOR_GREEN);
312 map.set_pair(b'Y', ASCII_8_COLOR_GREEN);
313 map.set_pair(b'M', ASCII_8_COLOR_GREEN);
314 map.set_pair(b'W', ASCII_8_COLOR_GREEN);
315 map.set_pair(b'N', ASCII_8_COLOR_MAGENTA);
316 map.set_pair(b'Q', ASCII_8_COLOR_MAGENTA);
317 map.set_pair(b'H', ASCII_8_COLOR_MAGENTA);
318 map.set_pair(b'D', ASCII_8_COLOR_RED);
319 map.set_pair(b'E', ASCII_8_COLOR_RED);
320 map.set_pair(b'K', ASCII_8_COLOR_BLUE);
321 map.set_pair(b'R', ASCII_8_COLOR_BLUE);
322 map.set_pair(b'X', RGB_WHITE);
323 map
324 }
325}
326
327impl Display for ResidueColorMap {
328 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
329 write!(f, "{{")?;
330 let residues = "ABCDEFGHIKLMNPQRSTUVWXY";
331 for c in residues.as_bytes().iter() {
332 write!(f, "{}: {}, ", *c as char, self.rgb(*c).to_hex())?;
333 }
334 write!(f, "}}")?;
335 Ok(())
336 }
337}
338
339#[cfg(test)]
340mod test {
341
342 use super::{ColorMapName, ResidueColorMap, Rgb, CLUSTALX_MAGENTA, GAP_COLOR, RGB_RED};
343
344 #[test]
345 fn test_default_colormap() {
346 let cmap = ResidueColorMap::with_default(GAP_COLOR, RGB_RED);
347 assert_eq!(RGB_RED, cmap.rgb(b'a'));
348 }
349
350 #[test]
351 fn test_simple_colormap() {
352 let cmap = ResidueColorMap::dna_basic();
353 assert_eq!(
354 Rgb {
355 r: 160,
356 g: 160,
357 b: 160
358 },
359 cmap.rgb(b'%')
360 );
361 assert_eq!(Rgb { r: 0, g: 200, b: 0 }, cmap.rgb(b'A'));
362 }
363
364 #[test]
365 fn test_aa_clustalx() {
366 let cmap = ResidueColorMap::aa_clustalx();
367 assert_eq!(CLUSTALX_MAGENTA, cmap.rgb(b'D'));
368 assert_eq!(CLUSTALX_MAGENTA, cmap.rgb(b'd'));
369 assert_eq!(GAP_COLOR, cmap.rgb(b'-'));
370 assert_eq!(GAP_COLOR, cmap.rgb(b'.'));
371 }
372
373 #[test]
374 fn test_from_u32() {
375 let rgb = Rgb::from_u32(0xFF7700);
376 assert_eq!(255, rgb.r);
377 assert_eq!(119, rgb.g);
378 assert_eq!(0, rgb.b);
379 }
380
381 #[test]
382 fn test_colormap_name() {
383 let cmap = ResidueColorMap::by_name(ColorMapName::AAClustalX);
384 assert_eq!(CLUSTALX_MAGENTA, cmap.rgb(b'D'));
385 assert_eq!(CLUSTALX_MAGENTA, cmap.rgb(b'd'));
386 assert_eq!(GAP_COLOR, cmap.rgb(b'-'));
387 assert_eq!(GAP_COLOR, cmap.rgb(b'.'));
388 }
389}