Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
FeatureColour | 55 | 282 | 124 |
1 | /* | |
2 | * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) | |
3 | * Copyright (C) $$Year-Rel$$ The Jalview Authors | |
4 | * | |
5 | * This file is part of Jalview. | |
6 | * | |
7 | * Jalview is free software: you can redistribute it and/or | |
8 | * modify it under the terms of the GNU General Public License | |
9 | * as published by the Free Software Foundation, either version 3 | |
10 | * of the License, or (at your option) any later version. | |
11 | * | |
12 | * Jalview is distributed in the hope that it will be useful, but | |
13 | * WITHOUT ANY WARRANTY; without even the implied warranty | |
14 | * of MERCHANTABILITY or FITNESS FOR A PARTICULAR | |
15 | * PURPOSE. See the GNU General Public License for more details. | |
16 | * | |
17 | * You should have received a copy of the GNU General Public License | |
18 | * along with Jalview. If not, see <http://www.gnu.org/licenses/>. | |
19 | * The Jalview Authors are detailed in the 'AUTHORS' file. | |
20 | */ | |
21 | package jalview.schemes; | |
22 | ||
23 | import java.util.Locale; | |
24 | ||
25 | import jalview.api.FeatureColourI; | |
26 | import jalview.datamodel.SequenceFeature; | |
27 | import jalview.datamodel.features.FeatureMatcher; | |
28 | import jalview.util.ColorUtils; | |
29 | import jalview.util.Format; | |
30 | import jalview.util.MessageManager; | |
31 | ||
32 | import java.awt.Color; | |
33 | import java.util.StringTokenizer; | |
34 | ||
35 | /** | |
36 | * A class that represents a colour scheme for a feature type. Options supported | |
37 | * are currently | |
38 | * <ul> | |
39 | * <li>a simple colour e.g. Red</li> | |
40 | * <li>colour by label - a colour is generated from the feature description</li> | |
41 | * <li>graduated colour by feature score</li> | |
42 | * <ul> | |
43 | * <li>minimum and maximum score range must be provided</li> | |
44 | * <li>minimum and maximum value colours should be specified</li> | |
45 | * <li>a colour for 'no value' may optionally be provided</li> | |
46 | * <li>colours for intermediate scores are interpolated RGB values</li> | |
47 | * <li>there is an optional threshold above/below which to colour values</li> | |
48 | * <li>the range may be the full value range, or may be limited by the threshold | |
49 | * value</li> | |
50 | * </ul> | |
51 | * <li>colour by (text) value of a named attribute</li> | |
52 | * <li>graduated colour by (numeric) value of a named attribute</li> | |
53 | * </ul> | |
54 | */ | |
55 | public class FeatureColour implements FeatureColourI | |
56 | { | |
57 | private static final String I18N_LABEL = MessageManager | |
58 | .getString("label.label"); | |
59 | ||
60 | private static final String I18N_SCORE = MessageManager | |
61 | .getString("label.score"); | |
62 | ||
63 | private static final String ABSOLUTE = "abso"; | |
64 | ||
65 | private static final String ABOVE = "above"; | |
66 | ||
67 | private static final String BELOW = "below"; | |
68 | ||
69 | /* | |
70 | * constants used to read or write a Jalview Features file | |
71 | */ | |
72 | private static final String LABEL = "label"; | |
73 | ||
74 | private static final String SCORE = "score"; | |
75 | ||
76 | private static final String ATTRIBUTE = "attribute"; | |
77 | ||
78 | private static final String NO_VALUE_MIN = "noValueMin"; | |
79 | ||
80 | private static final String NO_VALUE_MAX = "noValueMax"; | |
81 | ||
82 | private static final String NO_VALUE_NONE = "noValueNone"; | |
83 | ||
84 | static final Color DEFAULT_NO_COLOUR = null; | |
85 | ||
86 | private static final String BAR = "|"; | |
87 | ||
88 | final private Color colour; | |
89 | ||
90 | final private Color minColour; | |
91 | ||
92 | final private Color maxColour; | |
93 | ||
94 | /* | |
95 | * colour to use for colour by attribute when the | |
96 | * attribute value is absent | |
97 | */ | |
98 | final private Color noColour; | |
99 | ||
100 | /* | |
101 | * if true, then colour has a gradient based on a numerical | |
102 | * range (either feature score, or an attribute value) | |
103 | */ | |
104 | private boolean graduatedColour; | |
105 | ||
106 | /* | |
107 | * if true, colour values are generated from a text string, | |
108 | * either feature description, or an attribute value | |
109 | */ | |
110 | private boolean colourByLabel; | |
111 | ||
112 | /* | |
113 | * if not null, the value of [attribute, [sub-attribute] ...] | |
114 | * is used for colourByLabel or graduatedColour | |
115 | */ | |
116 | private String[] attributeName; | |
117 | ||
118 | private float threshold; | |
119 | ||
120 | private float base; | |
121 | ||
122 | private float range; | |
123 | ||
124 | private boolean belowThreshold; | |
125 | ||
126 | private boolean aboveThreshold; | |
127 | ||
128 | private boolean isHighToLow; | |
129 | ||
130 | private boolean autoScaled; | |
131 | ||
132 | final private float minRed; | |
133 | ||
134 | final private float minGreen; | |
135 | ||
136 | final private float minBlue; | |
137 | ||
138 | final private float deltaRed; | |
139 | ||
140 | final private float deltaGreen; | |
141 | ||
142 | final private float deltaBlue; | |
143 | ||
144 | /** | |
145 | * Parses a Jalview features file format colour descriptor | |
146 | * <p> | |
147 | * <code> | |
148 | * [label|score|[attribute|attributeName]|][mincolour|maxcolour| | |
149 | * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code> | |
150 | * <p> | |
151 | * 'Score' is optional (default) for a graduated colour. An attribute with | |
152 | * sub-attribute should be written as (for example) CSQ:Consequence. | |
153 | * noValueOption is one of <code>noValueMin, noValueMax, noValueNone</code> | |
154 | * with default noValueMin. | |
155 | * <p> | |
156 | * Examples: | |
157 | * <ul> | |
158 | * <li>red</li> | |
159 | * <li>a28bbb</li> | |
160 | * <li>25,125,213</li> | |
161 | * <li>label</li> | |
162 | * <li>attribute|CSQ:PolyPhen</li> | |
163 | * <li>label|||0.0|0.0|above|12.5</li> | |
164 | * <li>label|||0.0|0.0|below|12.5</li> | |
165 | * <li>red|green|12.0|26.0|none</li> | |
166 | * <li>score|red|green|12.0|26.0|none</li> | |
167 | * <li>attribute|AF|red|green|12.0|26.0|none</li> | |
168 | * <li>attribute|AF|red|green|noValueNone|12.0|26.0|none</li> | |
169 | * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li> | |
170 | * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li> | |
171 | * </ul> | |
172 | * | |
173 | * @param descriptor | |
174 | * @return | |
175 | * @throws IllegalArgumentException | |
176 | * if not parseable | |
177 | */ | |
178 | 70 | public static FeatureColourI parseJalviewFeatureColour(String descriptor) |
179 | { | |
180 | 70 | StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true); |
181 | 70 | float min = Float.MIN_VALUE; |
182 | 70 | float max = Float.MAX_VALUE; |
183 | 70 | boolean byLabel = false; |
184 | 70 | boolean byAttribute = false; |
185 | 70 | String attName = null; |
186 | 70 | String mincol = null; |
187 | 70 | String maxcol = null; |
188 | ||
189 | /* | |
190 | * first token should be 'label', or 'score', or an | |
191 | * attribute name, or simple colour, or minimum colour | |
192 | */ | |
193 | 70 | String nextToken = gcol.nextToken(); |
194 | 70 | if (nextToken == BAR) |
195 | { | |
196 | 0 | throw new IllegalArgumentException( |
197 | "Expected either 'label' or a colour specification in the line: " | |
198 | + descriptor); | |
199 | } | |
200 | 70 | if (nextToken.toLowerCase(Locale.ROOT).startsWith(LABEL)) |
201 | { | |
202 | 2 | byLabel = true; |
203 | // get the token after the next delimiter: | |
204 | 2 | mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); |
205 | 2 | mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); |
206 | } | |
207 | 68 | else if (nextToken.toLowerCase(Locale.ROOT).startsWith(SCORE)) |
208 | { | |
209 | 1 | mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); |
210 | 1 | mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); |
211 | } | |
212 | 67 | else if (nextToken.toLowerCase(Locale.ROOT).startsWith(ATTRIBUTE)) |
213 | { | |
214 | 4 | byAttribute = true; |
215 | 4 | attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null); |
216 | 4 | attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null); |
217 | 4 | mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); |
218 | 4 | mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); |
219 | } | |
220 | else | |
221 | { | |
222 | 63 | mincol = nextToken; |
223 | } | |
224 | ||
225 | /* | |
226 | * if only one token, it can validly be label, attributeName, | |
227 | * or a plain colour value | |
228 | */ | |
229 | 70 | if (!gcol.hasMoreTokens()) |
230 | { | |
231 | 55 | if (byLabel || byAttribute) |
232 | { | |
233 | 2 | FeatureColourI fc = new FeatureColour(); |
234 | 2 | fc.setColourByLabel(true); |
235 | 2 | if (byAttribute) |
236 | { | |
237 | 1 | fc.setAttributeName( |
238 | FeatureMatcher.fromAttributeDisplayName(attName)); | |
239 | } | |
240 | 2 | return fc; |
241 | } | |
242 | ||
243 | 53 | Color colour = ColorUtils.parseColourString(descriptor); |
244 | 53 | if (colour == null) |
245 | { | |
246 | 1 | throw new IllegalArgumentException( |
247 | "Invalid colour descriptor: " + descriptor); | |
248 | } | |
249 | 52 | return new FeatureColour(colour); |
250 | } | |
251 | ||
252 | /* | |
253 | * continue parsing for min/max/no colour (if graduated) | |
254 | * and for threshold (colour by text or graduated) | |
255 | */ | |
256 | ||
257 | /* | |
258 | * autoScaled == true: colours range over actual score range | |
259 | * autoScaled == false ('abso'): colours range over min/max range | |
260 | */ | |
261 | 15 | boolean autoScaled = true; |
262 | 15 | String tok = null, minval, maxval; |
263 | 15 | String noValueColour = NO_VALUE_MIN; |
264 | ||
265 | 15 | if (mincol != null) |
266 | { | |
267 | // at least four more tokens | |
268 | 15 | if (mincol.equals(BAR)) |
269 | { | |
270 | 2 | mincol = null; |
271 | } | |
272 | else | |
273 | { | |
274 | 13 | gcol.nextToken(); // skip next '|' |
275 | } | |
276 | 15 | maxcol = gcol.nextToken(); |
277 | 15 | if (maxcol.equals(BAR)) |
278 | { | |
279 | 2 | maxcol = null; |
280 | } | |
281 | else | |
282 | { | |
283 | 13 | gcol.nextToken(); // skip next '|' |
284 | } | |
285 | 15 | tok = gcol.nextToken(); |
286 | ||
287 | /* | |
288 | * check for specifier for colour for no attribute value | |
289 | * (new in 2.11, defaults to minColour if not specified) | |
290 | */ | |
291 | 15 | if (tok.equalsIgnoreCase(NO_VALUE_MIN)) |
292 | { | |
293 | 1 | tok = gcol.nextToken(); |
294 | 1 | tok = gcol.nextToken(); |
295 | } | |
296 | 14 | else if (tok.equalsIgnoreCase(NO_VALUE_MAX)) |
297 | { | |
298 | 1 | noValueColour = NO_VALUE_MAX; |
299 | 1 | tok = gcol.nextToken(); |
300 | 1 | tok = gcol.nextToken(); |
301 | } | |
302 | 13 | else if (tok.equalsIgnoreCase(NO_VALUE_NONE)) |
303 | { | |
304 | 1 | noValueColour = NO_VALUE_NONE; |
305 | 1 | tok = gcol.nextToken(); |
306 | 1 | tok = gcol.nextToken(); |
307 | } | |
308 | ||
309 | 15 | gcol.nextToken(); // skip next '|' |
310 | 15 | if (tok.toLowerCase(Locale.ROOT).startsWith(ABSOLUTE)) |
311 | { | |
312 | 2 | minval = gcol.nextToken(); |
313 | 2 | gcol.nextToken(); // skip next '|' |
314 | 2 | autoScaled = false; |
315 | } | |
316 | else | |
317 | { | |
318 | 13 | minval = tok; |
319 | } | |
320 | 15 | maxval = gcol.nextToken(); |
321 | 15 | if (gcol.hasMoreTokens()) |
322 | { | |
323 | 9 | gcol.nextToken(); // skip next '|' |
324 | } | |
325 | 15 | try |
326 | { | |
327 | 15 | if (minval.length() > 0) |
328 | { | |
329 | 15 | min = Float.valueOf(minval).floatValue(); |
330 | } | |
331 | } catch (Exception e) | |
332 | { | |
333 | 1 | throw new IllegalArgumentException( |
334 | "Couldn't parse the minimum value for graduated colour ('" | |
335 | + minval + "')"); | |
336 | } | |
337 | 14 | try |
338 | { | |
339 | 14 | if (maxval.length() > 0) |
340 | { | |
341 | 14 | max = Float.valueOf(maxval).floatValue(); |
342 | } | |
343 | } catch (Exception e) | |
344 | { | |
345 | 0 | throw new IllegalArgumentException( |
346 | "Couldn't parse the maximum value for graduated colour (" | |
347 | + descriptor + ")"); | |
348 | } | |
349 | } | |
350 | else | |
351 | { | |
352 | /* | |
353 | * dummy min/max colours for colour by text | |
354 | * (label or attribute value) | |
355 | */ | |
356 | 0 | mincol = "white"; |
357 | 0 | maxcol = "black"; |
358 | 0 | byLabel = true; |
359 | } | |
360 | ||
361 | /* | |
362 | * construct the FeatureColour! | |
363 | */ | |
364 | 14 | FeatureColour featureColour; |
365 | 14 | try |
366 | { | |
367 | 14 | Color minColour = ColorUtils.parseColourString(mincol); |
368 | 14 | Color maxColour = ColorUtils.parseColourString(maxcol); |
369 | 14 | Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour |
370 | 13 | : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour); |
371 | 14 | featureColour = new FeatureColour(maxColour, minColour, maxColour, |
372 | noColour, min, max); | |
373 | 14 | featureColour.setColourByLabel(minColour == null); |
374 | 14 | featureColour.setAutoScaled(autoScaled); |
375 | 14 | if (byAttribute) |
376 | { | |
377 | 3 | featureColour.setAttributeName( |
378 | FeatureMatcher.fromAttributeDisplayName(attName)); | |
379 | } | |
380 | // add in any additional parameters | |
381 | 14 | String ttype = null, tval = null; |
382 | 14 | if (gcol.hasMoreTokens()) |
383 | { | |
384 | // threshold type and possibly a threshold value | |
385 | 8 | ttype = gcol.nextToken(); |
386 | 8 | if (ttype.toLowerCase(Locale.ROOT).startsWith(BELOW)) |
387 | { | |
388 | 4 | featureColour.setBelowThreshold(true); |
389 | } | |
390 | 4 | else if (ttype.toLowerCase(Locale.ROOT).startsWith(ABOVE)) |
391 | { | |
392 | 4 | featureColour.setAboveThreshold(true); |
393 | } | |
394 | else | |
395 | { | |
396 | 0 | if (!ttype.toLowerCase(Locale.ROOT).startsWith("no")) |
397 | { | |
398 | 0 | jalview.bin.Console.errPrintln( |
399 | "Ignoring unrecognised threshold type : " + ttype); | |
400 | } | |
401 | } | |
402 | } | |
403 | 14 | if (featureColour.hasThreshold()) |
404 | { | |
405 | 8 | try |
406 | { | |
407 | 8 | gcol.nextToken(); |
408 | 8 | tval = gcol.nextToken(); |
409 | 8 | featureColour.setThreshold(Float.valueOf(tval).floatValue()); |
410 | } catch (Exception e) | |
411 | { | |
412 | 0 | jalview.bin.Console |
413 | .errPrintln("Couldn't parse threshold value as a float: (" | |
414 | + tval + ")"); | |
415 | } | |
416 | } | |
417 | 14 | if (gcol.hasMoreTokens()) |
418 | { | |
419 | 0 | jalview.bin.Console.errPrintln( |
420 | "Ignoring additional tokens in parameters in graduated colour specification\n"); | |
421 | 0 | while (gcol.hasMoreTokens()) |
422 | { | |
423 | 0 | jalview.bin.Console.errPrintln(BAR + gcol.nextToken()); |
424 | } | |
425 | 0 | jalview.bin.Console.errPrintln("\n"); |
426 | } | |
427 | 14 | return featureColour; |
428 | } catch (Exception e) | |
429 | { | |
430 | 0 | throw new IllegalArgumentException(e.getMessage()); |
431 | } | |
432 | } | |
433 | ||
434 | /** | |
435 | * Default constructor | |
436 | */ | |
437 | 36 | public FeatureColour() |
438 | { | |
439 | 36 | this((Color) null); |
440 | } | |
441 | ||
442 | /** | |
443 | * Constructor given a simple colour. This also 'primes' a graduated colour | |
444 | * range, where the maximum colour is the given simple colour, and the minimum | |
445 | * colour a paler shade of it. This is for convenience when switching from a | |
446 | * simple colour to a graduated colour scheme. | |
447 | * | |
448 | * @param c | |
449 | */ | |
450 | 1002 | public FeatureColour(Color c) |
451 | { | |
452 | /* | |
453 | * set max colour to the simple colour, min colour to a paler shade of it | |
454 | */ | |
455 | 1002 | this(c, c == null ? Color.white : ColorUtils.bleachColour(c, 0.9f), |
456 | 1002 | c == null ? Color.black : c, DEFAULT_NO_COLOUR, 0, 0); |
457 | ||
458 | /* | |
459 | * but enforce simple colour for now! | |
460 | */ | |
461 | 1002 | setGraduatedColour(false); |
462 | } | |
463 | ||
464 | /** | |
465 | * Copy constructor | |
466 | * | |
467 | * @param fc | |
468 | */ | |
469 | 10 | public FeatureColour(FeatureColour fc) |
470 | { | |
471 | 10 | graduatedColour = fc.graduatedColour; |
472 | 10 | colour = fc.colour; |
473 | 10 | minColour = fc.minColour; |
474 | 10 | maxColour = fc.maxColour; |
475 | 10 | noColour = fc.noColour; |
476 | 10 | minRed = fc.minRed; |
477 | 10 | minGreen = fc.minGreen; |
478 | 10 | minBlue = fc.minBlue; |
479 | 10 | deltaRed = fc.deltaRed; |
480 | 10 | deltaGreen = fc.deltaGreen; |
481 | 10 | deltaBlue = fc.deltaBlue; |
482 | 10 | base = fc.base; |
483 | 10 | range = fc.range; |
484 | 10 | isHighToLow = fc.isHighToLow; |
485 | 10 | attributeName = fc.attributeName; |
486 | 10 | setAboveThreshold(fc.isAboveThreshold()); |
487 | 10 | setBelowThreshold(fc.isBelowThreshold()); |
488 | 10 | setThreshold(fc.getThreshold()); |
489 | 10 | setAutoScaled(fc.isAutoScaled()); |
490 | 10 | setColourByLabel(fc.isColourByLabel()); |
491 | } | |
492 | ||
493 | /** | |
494 | * Constructor that sets both simple and graduated colour values. This allows | |
495 | * alternative colour schemes to be 'preserved' while switching between them | |
496 | * to explore their effects on the visualisation. | |
497 | * <p> | |
498 | * This sets the colour scheme to 'graduated' by default. Override this if | |
499 | * wanted by calling <code>setGraduatedColour(false)</code> for a simple | |
500 | * colour, or <code>setColourByLabel(true)</code> for colour by label. | |
501 | * | |
502 | * @param myColour | |
503 | * @param low | |
504 | * @param high | |
505 | * @param noValueColour | |
506 | * @param min | |
507 | * @param max | |
508 | */ | |
509 | 1055 | public FeatureColour(Color myColour, Color low, Color high, |
510 | Color noValueColour, float min, float max) | |
511 | { | |
512 | 1055 | if (low == null) |
513 | { | |
514 | 2 | low = Color.white; |
515 | } | |
516 | 1055 | if (high == null) |
517 | { | |
518 | 2 | high = Color.black; |
519 | } | |
520 | 1055 | colour = myColour; |
521 | 1055 | minColour = low; |
522 | 1055 | maxColour = high; |
523 | 1055 | setGraduatedColour(true); |
524 | 1055 | noColour = noValueColour; |
525 | 1055 | threshold = Float.NaN; |
526 | 1055 | isHighToLow = min >= max; |
527 | 1055 | minRed = low.getRed() / 255f; |
528 | 1055 | minGreen = low.getGreen() / 255f; |
529 | 1055 | minBlue = low.getBlue() / 255f; |
530 | 1055 | deltaRed = (high.getRed() / 255f) - minRed; |
531 | 1055 | deltaGreen = (high.getGreen() / 255f) - minGreen; |
532 | 1055 | deltaBlue = (high.getBlue() / 255f) - minBlue; |
533 | 1055 | if (isHighToLow) |
534 | { | |
535 | 1009 | base = max; |
536 | 1009 | range = min - max; |
537 | } | |
538 | else | |
539 | { | |
540 | 46 | base = min; |
541 | 46 | range = max - min; |
542 | } | |
543 | } | |
544 | ||
545 | 28660 | @Override |
546 | public boolean isGraduatedColour() | |
547 | { | |
548 | 28661 | return graduatedColour; |
549 | } | |
550 | ||
551 | /** | |
552 | * Sets the 'graduated colour' flag. If true, also sets 'colour by label' to | |
553 | * false. | |
554 | */ | |
555 | 2087 | public void setGraduatedColour(boolean b) |
556 | { | |
557 | 2087 | graduatedColour = b; |
558 | 2087 | if (b) |
559 | { | |
560 | 1055 | setColourByLabel(false); |
561 | } | |
562 | } | |
563 | ||
564 | 25547 | @Override |
565 | public Color getColour() | |
566 | { | |
567 | 25547 | return colour; |
568 | } | |
569 | ||
570 | 36 | @Override |
571 | public Color getMinColour() | |
572 | { | |
573 | 36 | return minColour; |
574 | } | |
575 | ||
576 | 36 | @Override |
577 | public Color getMaxColour() | |
578 | { | |
579 | 36 | return maxColour; |
580 | } | |
581 | ||
582 | 15 | @Override |
583 | public Color getNoColour() | |
584 | { | |
585 | 15 | return noColour; |
586 | } | |
587 | ||
588 | 28679 | @Override |
589 | public boolean isColourByLabel() | |
590 | { | |
591 | 28679 | return colourByLabel; |
592 | } | |
593 | ||
594 | /** | |
595 | * Sets the 'colour by label' flag. If true, also sets 'graduated colour' to | |
596 | * false. | |
597 | */ | |
598 | 1105 | @Override |
599 | public void setColourByLabel(boolean b) | |
600 | { | |
601 | 1105 | colourByLabel = b; |
602 | 1105 | if (b) |
603 | { | |
604 | 30 | setGraduatedColour(false); |
605 | } | |
606 | } | |
607 | ||
608 | 132 | @Override |
609 | public boolean isBelowThreshold() | |
610 | { | |
611 | 132 | return belowThreshold; |
612 | } | |
613 | ||
614 | 64 | @Override |
615 | public void setBelowThreshold(boolean b) | |
616 | { | |
617 | 64 | belowThreshold = b; |
618 | 64 | if (b) |
619 | { | |
620 | 20 | setAboveThreshold(false); |
621 | } | |
622 | } | |
623 | ||
624 | 171 | @Override |
625 | public boolean isAboveThreshold() | |
626 | { | |
627 | 171 | return aboveThreshold; |
628 | } | |
629 | ||
630 | 65 | @Override |
631 | public void setAboveThreshold(boolean b) | |
632 | { | |
633 | 65 | aboveThreshold = b; |
634 | 65 | if (b) |
635 | { | |
636 | 34 | setBelowThreshold(false); |
637 | } | |
638 | } | |
639 | ||
640 | 52 | @Override |
641 | public float getThreshold() | |
642 | { | |
643 | 52 | return threshold; |
644 | } | |
645 | ||
646 | 58 | @Override |
647 | public void setThreshold(float f) | |
648 | { | |
649 | 58 | threshold = f; |
650 | } | |
651 | ||
652 | 63 | @Override |
653 | public boolean isAutoScaled() | |
654 | { | |
655 | 63 | return autoScaled; |
656 | } | |
657 | ||
658 | 48 | @Override |
659 | public void setAutoScaled(boolean b) | |
660 | { | |
661 | 48 | this.autoScaled = b; |
662 | } | |
663 | ||
664 | /** | |
665 | * {@inheritDoc} | |
666 | */ | |
667 | 0 | @Override |
668 | public void updateBounds(float min, float max) | |
669 | { | |
670 | 0 | if (max < min) |
671 | { | |
672 | 0 | base = max; |
673 | 0 | range = min - max; |
674 | 0 | isHighToLow = true; |
675 | } | |
676 | else | |
677 | { | |
678 | 0 | base = min; |
679 | 0 | range = max - min; |
680 | 0 | isHighToLow = false; |
681 | } | |
682 | } | |
683 | ||
684 | /** | |
685 | * Returns the colour for the given instance of the feature. This may be a | |
686 | * simple colour, a colour generated from the feature description or other | |
687 | * attribute (if isColourByLabel()), or a colour derived from the feature | |
688 | * score or other attribute (if isGraduatedColour()). | |
689 | * <p> | |
690 | * Answers null if feature score (or attribute) value lies outside a | |
691 | * configured threshold. | |
692 | * | |
693 | * @param feature | |
694 | * @return | |
695 | */ | |
696 | 25385 | @Override |
697 | public Color getColor(SequenceFeature feature) | |
698 | { | |
699 | 25384 | if (isColourByLabel()) |
700 | { | |
701 | 15 | String label = attributeName == null ? feature.getDescription() |
702 | : feature.getValueAsString(attributeName); | |
703 | 15 | return label == null ? noColour |
704 | : ColorUtils.createColourFromName(label); | |
705 | } | |
706 | ||
707 | 25370 | if (!isGraduatedColour()) |
708 | { | |
709 | 25325 | return getColour(); |
710 | } | |
711 | ||
712 | /* | |
713 | * graduated colour case, optionally with threshold | |
714 | * may be based on feature score on an attribute value | |
715 | * Float.NaN, or no value, is assigned the 'no value' colour | |
716 | */ | |
717 | 45 | float scr = feature.getScore(); |
718 | 45 | if (attributeName != null) |
719 | { | |
720 | 13 | try |
721 | { | |
722 | 13 | String attVal = feature.getValueAsString(attributeName); |
723 | 13 | scr = Float.valueOf(attVal); |
724 | } catch (Throwable e) | |
725 | { | |
726 | 5 | scr = Float.NaN; |
727 | } | |
728 | } | |
729 | 45 | if (Float.isNaN(scr)) |
730 | { | |
731 | 10 | return noColour; |
732 | } | |
733 | ||
734 | 35 | if (isAboveThreshold() && scr <= threshold) |
735 | { | |
736 | 10 | return null; |
737 | } | |
738 | ||
739 | 25 | if (isBelowThreshold() && scr >= threshold) |
740 | { | |
741 | 5 | return null; |
742 | } | |
743 | 20 | if (range == 0.0) |
744 | { | |
745 | 0 | return getMaxColour(); |
746 | } | |
747 | 20 | float scl = (scr - base) / range; |
748 | 20 | if (isHighToLow) |
749 | { | |
750 | 0 | scl = -scl; |
751 | } | |
752 | 20 | if (scl < 0f) |
753 | { | |
754 | 0 | scl = 0f; |
755 | } | |
756 | 20 | if (scl > 1f) |
757 | { | |
758 | 0 | scl = 1f; |
759 | } | |
760 | 20 | return new Color(minRed + scl * deltaRed, minGreen + scl * deltaGreen, |
761 | minBlue + scl * deltaBlue); | |
762 | } | |
763 | ||
764 | /** | |
765 | * Returns the maximum score of the graduated colour range | |
766 | * | |
767 | * @return | |
768 | */ | |
769 | 37 | @Override |
770 | public float getMax() | |
771 | { | |
772 | // regenerate the original values passed in to the constructor | |
773 | 37 | return (isHighToLow) ? base : (base + range); |
774 | } | |
775 | ||
776 | /** | |
777 | * Returns the minimum score of the graduated colour range | |
778 | * | |
779 | * @return | |
780 | */ | |
781 | 37 | @Override |
782 | public float getMin() | |
783 | { | |
784 | // regenerate the original value passed in to the constructor | |
785 | 37 | return (isHighToLow) ? (base + range) : base; |
786 | } | |
787 | ||
788 | 1889 | @Override |
789 | public boolean isSimpleColour() | |
790 | { | |
791 | 1889 | return (!isColourByLabel() && !isGraduatedColour()); |
792 | } | |
793 | ||
794 | 48 | @Override |
795 | public boolean hasThreshold() | |
796 | { | |
797 | 48 | return isAboveThreshold() || isBelowThreshold(); |
798 | } | |
799 | ||
800 | 29 | @Override |
801 | public String toJalviewFormat(String featureType) | |
802 | { | |
803 | 29 | String colourString = null; |
804 | 29 | if (isSimpleColour()) |
805 | { | |
806 | 9 | colourString = Format.getHexString(getColour()); |
807 | } | |
808 | else | |
809 | { | |
810 | 20 | StringBuilder sb = new StringBuilder(32); |
811 | 20 | if (isColourByAttribute()) |
812 | { | |
813 | 3 | sb.append(ATTRIBUTE).append(BAR); |
814 | 3 | sb.append( |
815 | FeatureMatcher.toAttributeDisplayName(getAttributeName())); | |
816 | } | |
817 | 17 | else if (isColourByLabel()) |
818 | { | |
819 | 4 | sb.append(LABEL); |
820 | } | |
821 | else | |
822 | { | |
823 | 13 | sb.append(SCORE); |
824 | } | |
825 | 20 | if (isGraduatedColour()) |
826 | { | |
827 | 14 | sb.append(BAR).append(Format.getHexString(getMinColour())) |
828 | .append(BAR); | |
829 | 14 | sb.append(Format.getHexString(getMaxColour())).append(BAR); |
830 | ||
831 | /* | |
832 | * 'no value' colour should be null, min or max colour; | |
833 | * if none of these, coerce to minColour | |
834 | */ | |
835 | 14 | String noValue = NO_VALUE_MIN; |
836 | 14 | if (maxColour.equals(noColour)) |
837 | { | |
838 | 5 | noValue = NO_VALUE_MAX; |
839 | } | |
840 | 14 | if (noColour == null) |
841 | { | |
842 | 0 | noValue = NO_VALUE_NONE; |
843 | } | |
844 | 14 | sb.append(noValue).append(BAR); |
845 | 14 | if (!isAutoScaled()) |
846 | { | |
847 | 8 | sb.append(ABSOLUTE).append(BAR); |
848 | } | |
849 | } | |
850 | else | |
851 | { | |
852 | /* | |
853 | * colour by text with score threshold: empty fields for | |
854 | * minColour and maxColour (not used) | |
855 | */ | |
856 | 6 | if (hasThreshold()) |
857 | { | |
858 | 3 | sb.append(BAR).append(BAR).append(BAR); |
859 | } | |
860 | } | |
861 | 20 | if (hasThreshold() || isGraduatedColour()) |
862 | { | |
863 | 17 | sb.append(getMin()).append(BAR); |
864 | 17 | sb.append(getMax()).append(BAR); |
865 | 17 | if (isBelowThreshold()) |
866 | { | |
867 | 7 | sb.append(BELOW).append(BAR).append(getThreshold()); |
868 | } | |
869 | 10 | else if (isAboveThreshold()) |
870 | { | |
871 | 4 | sb.append(ABOVE).append(BAR).append(getThreshold()); |
872 | } | |
873 | else | |
874 | { | |
875 | 6 | sb.append("none"); |
876 | } | |
877 | } | |
878 | 20 | colourString = sb.toString(); |
879 | } | |
880 | 29 | return String.format("%s\t%s", featureType, colourString); |
881 | } | |
882 | ||
883 | 49 | @Override |
884 | public boolean isColourByAttribute() | |
885 | { | |
886 | 49 | return attributeName != null; |
887 | } | |
888 | ||
889 | 49 | @Override |
890 | public String[] getAttributeName() | |
891 | { | |
892 | 49 | return attributeName; |
893 | } | |
894 | ||
895 | 33 | @Override |
896 | public void setAttributeName(String... name) | |
897 | { | |
898 | 33 | attributeName = name; |
899 | } | |
900 | ||
901 | 51 | @Override |
902 | public boolean isOutwithThreshold(SequenceFeature feature) | |
903 | { | |
904 | 51 | if (!isGraduatedColour()) |
905 | { | |
906 | 21 | return false; |
907 | } | |
908 | 30 | float scr = feature.getScore(); |
909 | 30 | if (attributeName != null) |
910 | { | |
911 | 9 | try |
912 | { | |
913 | 9 | String attVal = feature.getValueAsString(attributeName); |
914 | 9 | scr = Float.valueOf(attVal); |
915 | } catch (Throwable e) | |
916 | { | |
917 | 5 | scr = Float.NaN; |
918 | } | |
919 | } | |
920 | 30 | if (Float.isNaN(scr)) |
921 | { | |
922 | 5 | return false; |
923 | } | |
924 | ||
925 | 25 | return ((isAboveThreshold() && scr <= threshold) |
926 | || (isBelowThreshold() && scr >= threshold)); | |
927 | } | |
928 | ||
929 | 17 | @Override |
930 | public String getDescription() | |
931 | { | |
932 | 17 | if (isSimpleColour()) |
933 | { | |
934 | 1 | return "r=" + colour.getRed() + ",g=" + colour.getGreen() + ",b=" |
935 | + colour.getBlue(); | |
936 | } | |
937 | 16 | StringBuilder tt = new StringBuilder(); |
938 | 16 | String by = null; |
939 | ||
940 | 16 | if (getAttributeName() != null) |
941 | { | |
942 | 4 | by = FeatureMatcher.toAttributeDisplayName(getAttributeName()); |
943 | } | |
944 | 12 | else if (isColourByLabel()) |
945 | { | |
946 | 5 | by = I18N_LABEL; |
947 | } | |
948 | else | |
949 | { | |
950 | 7 | by = I18N_SCORE; |
951 | } | |
952 | 16 | tt.append(MessageManager.formatMessage("action.by_title_param", by)); |
953 | ||
954 | /* | |
955 | * add threshold if any | |
956 | */ | |
957 | 16 | if (isAboveThreshold() || isBelowThreshold()) |
958 | { | |
959 | 10 | tt.append(" ("); |
960 | 10 | if (isColourByLabel()) |
961 | { | |
962 | /* | |
963 | * Jalview features file supports the combination of | |
964 | * colour by label or attribute text with score threshold | |
965 | */ | |
966 | 3 | tt.append(I18N_SCORE).append(" "); |
967 | } | |
968 | 10 | tt.append(isAboveThreshold() ? "> " : "< "); |
969 | 10 | tt.append(getThreshold()).append(")"); |
970 | } | |
971 | ||
972 | 16 | return tt.toString(); |
973 | } | |
974 | ||
975 | } |