JAL-2624 check feature colour threshold when colouring structure
[jalview.git] / src / jalview / renderer / seqfeatures / FeatureRenderer.java
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.renderer.seqfeatures;
22
23 import jalview.api.AlignViewportI;
24 import jalview.datamodel.SequenceFeature;
25 import jalview.datamodel.SequenceI;
26 import jalview.util.Comparison;
27 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
28
29 import java.awt.AlphaComposite;
30 import java.awt.Color;
31 import java.awt.FontMetrics;
32 import java.awt.Graphics;
33 import java.awt.Graphics2D;
34
35 public class FeatureRenderer extends FeatureRendererModel
36 {
37   private static final AlphaComposite NO_TRANSPARENCY = AlphaComposite
38           .getInstance(AlphaComposite.SRC_OVER, 1.0f);
39
40   /**
41    * Constructor given a viewport
42    * 
43    * @param viewport
44    */
45   public FeatureRenderer(AlignViewportI viewport)
46   {
47     this.av = viewport;
48   }
49
50   /**
51    * Renders the sequence using the given feature colour between the given start
52    * and end columns. Returns true if at least one column is drawn, else false
53    * (the feature range does not overlap the start and end positions).
54    * 
55    * @param g
56    * @param seq
57    * @param featureStart
58    * @param featureEnd
59    * @param featureColour
60    * @param start
61    * @param end
62    * @param y1
63    * @param colourOnly
64    * @return
65    */
66   boolean renderFeature(Graphics g, SequenceI seq, int featureStart,
67           int featureEnd, Color featureColour, int start, int end, int y1,
68           boolean colourOnly)
69   {
70     int charHeight = av.getCharHeight();
71     int charWidth = av.getCharWidth();
72     boolean validCharWidth = av.isValidCharWidth();
73
74     if (featureStart > end || featureEnd < start)
75     {
76       return false;
77     }
78
79     if (featureStart < start)
80     {
81       featureStart = start;
82     }
83     if (featureEnd >= end)
84     {
85       featureEnd = end;
86     }
87     int pady = (y1 + charHeight) - charHeight / 5;
88
89     FontMetrics fm = g.getFontMetrics();
90     for (int i = featureStart; i <= featureEnd; i++)
91     {
92       char s = seq.getCharAt(i);
93
94       if (Comparison.isGap(s))
95       {
96         continue;
97       }
98
99       g.setColor(featureColour);
100
101       g.fillRect((i - start) * charWidth, y1, charWidth,
102               charHeight);
103
104       if (colourOnly || !validCharWidth)
105       {
106         continue;
107       }
108
109       g.setColor(Color.white);
110       int charOffset = (charWidth - fm.charWidth(s)) / 2;
111       g.drawString(String.valueOf(s), charOffset
112               + (charWidth * (i - start)), pady);
113     }
114     return true;
115   }
116
117   /**
118    * Renders the sequence using the given SCORE feature colour between the given
119    * start and end columns. Returns true if at least one column is drawn, else
120    * false (the feature range does not overlap the start and end positions).
121    * 
122    * @param g
123    * @param seq
124    * @param fstart
125    * @param fend
126    * @param featureColour
127    * @param start
128    * @param end
129    * @param y1
130    * @param bs
131    * @param colourOnly
132    * @return
133    */
134   boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
135           int fend, Color featureColour, int start, int end, int y1,
136           byte[] bs, boolean colourOnly)
137   {
138     if (fstart > end || fend < start)
139     {
140       return false;
141     }
142
143     if (fstart < start)
144     { // fix for if the feature we have starts before the sequence start,
145       fstart = start; // but the feature end is still valid!!
146     }
147
148     if (fend >= end)
149     {
150       fend = end;
151     }
152     int charHeight = av.getCharHeight();
153     int pady = (y1 + charHeight) - charHeight / 5;
154     int ystrt = 0, yend = charHeight;
155     if (bs[0] != 0)
156     {
157       // signed - zero is always middle of residue line.
158       if (bs[1] < 128)
159       {
160         yend = charHeight * (128 - bs[1]) / 512;
161         ystrt = charHeight - yend / 2;
162       }
163       else
164       {
165         ystrt = charHeight / 2;
166         yend = charHeight * (bs[1] - 128) / 512;
167       }
168     }
169     else
170     {
171       yend = charHeight * bs[1] / 255;
172       ystrt = charHeight - yend;
173
174     }
175
176     FontMetrics fm = g.getFontMetrics();
177     int charWidth = av.getCharWidth();
178
179     for (int i = fstart; i <= fend; i++)
180     {
181       char s = seq.getCharAt(i);
182
183       if (Comparison.isGap(s))
184       {
185         continue;
186       }
187
188       g.setColor(featureColour);
189       int x = (i - start) * charWidth;
190       g.drawRect(x, y1, charWidth, charHeight);
191       g.fillRect(x, y1 + ystrt, charWidth, yend);
192
193       if (colourOnly || !av.isValidCharWidth())
194       {
195         continue;
196       }
197
198       g.setColor(Color.black);
199       int charOffset = (charWidth - fm.charWidth(s)) / 2;
200       g.drawString(String.valueOf(s), charOffset
201               + (charWidth * (i - start)), pady);
202     }
203     return true;
204   }
205
206   /**
207    * {@inheritDoc}
208    */
209   @Override
210   public Color findFeatureColour(SequenceI seq, int column, Graphics g)
211   {
212     if (!av.isShowSequenceFeatures())
213     {
214       return null;
215     }
216
217     SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
218
219     if (sequenceFeatures == null || sequenceFeatures.length == 0)
220     {
221       return null;
222     }
223
224     if (Comparison.isGap(seq.getCharAt(column)))
225     {
226       /*
227        * returning null allows the colour scheme to provide gap colour
228        * - normally white, but can be customised otherwise
229        */
230       return null;
231     }
232
233     Color renderedColour = null;
234     if (transparency == 1.0f)
235     {
236       /*
237        * simple case - just find the topmost rendered visible feature colour
238        */
239       renderedColour = findFeatureColour(seq, seq.findPosition(column));
240     }
241     else
242     {
243       /*
244        * transparency case - draw all visible features in render order to
245        * build up a composite colour on the graphics context
246        */
247       renderedColour = drawSequence(g, seq, column, column, 0, true);
248     }
249     return renderedColour;
250   }
251
252   /**
253    * Draws the sequence features on the graphics context, or just determines the
254    * colour that would be drawn (if flag colourOnly is true). Returns the last
255    * colour drawn (which may not be the effective colour if transparency
256    * applies), or null if no feature is drawn in the range given.
257    * 
258    * @param g
259    *          the graphics context to draw on (may be null if colourOnly==true)
260    * @param seq
261    * @param start
262    *          start column
263    * @param end
264    *          end column
265    * @param y1
266    *          vertical offset at which to draw on the graphics
267    * @param colourOnly
268    *          if true, only do enough to determine the colour for the position,
269    *          do not draw the character
270    * @return
271    */
272   public synchronized Color drawSequence(final Graphics g,
273           final SequenceI seq, int start, int end, int y1,
274           boolean colourOnly)
275   {
276     SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
277     if (sequenceFeatures == null || sequenceFeatures.length == 0)
278     {
279       return null;
280     }
281
282     updateFeatures();
283
284     if (transparency != 1f && g != null)
285     {
286       Graphics2D g2 = (Graphics2D) g;
287       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
288               transparency));
289     }
290
291     int startPos = seq.findPosition(start);
292     int endPos = seq.findPosition(end);
293
294     int sfSize = sequenceFeatures.length;
295     Color drawnColour = null;
296
297     /*
298      * iterate over features in ordering of their rendering (last is on top)
299      */
300     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
301     {
302       String type = renderOrder[renderIndex];
303       if (!showFeatureOfType(type))
304       {
305         continue;
306       }
307
308       // loop through all features in sequence to find
309       // current feature to render
310       for (int sfindex = 0; sfindex < sfSize; sfindex++)
311       {
312         final SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
313         if (!sequenceFeature.type.equals(type))
314         {
315           continue;
316         }
317
318         /*
319          * a feature type may be flagged as shown but the group 
320          * an instance of it belongs to may be hidden
321          */
322         if (featureGroupNotShown(sequenceFeature))
323         {
324           continue;
325         }
326
327         /*
328          * check feature overlaps the target range
329          * TODO: efficient retrieval of features overlapping a range
330          */
331         if (sequenceFeature.getBegin() > endPos
332                 || sequenceFeature.getEnd() < startPos)
333         {
334           continue;
335         }
336
337         Color featureColour = getColour(sequenceFeature);
338         if (featureColour == null)
339         {
340           // score feature outwith threshold for colouring
341           continue;
342         }
343
344         boolean isContactFeature = sequenceFeature.isContactFeature();
345
346         if (isContactFeature)
347         {
348           boolean drawn = renderFeature(g, seq,
349                   seq.findIndex(sequenceFeature.begin) - 1,
350                   seq.findIndex(sequenceFeature.begin) - 1, featureColour,
351                   start, end, y1, colourOnly);
352           drawn |= renderFeature(g, seq,
353                   seq.findIndex(sequenceFeature.end) - 1,
354                   seq.findIndex(sequenceFeature.end) - 1, featureColour,
355                   start, end, y1, colourOnly);
356           if (drawn)
357           {
358             drawnColour = featureColour;
359           }
360         }
361         else
362         {
363           if (av.isShowSequenceFeaturesHeight()
364                   && !Float.isNaN(sequenceFeature.score))
365           {
366             boolean drawn = renderScoreFeature(g, seq,
367                     seq.findIndex(sequenceFeature.begin) - 1,
368                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
369                     start, end, y1, normaliseScore(sequenceFeature),
370                     colourOnly);
371             if (drawn)
372             {
373               drawnColour = featureColour;
374             }
375           }
376           else
377           {
378             boolean drawn = renderFeature(g, seq,
379                     seq.findIndex(sequenceFeature.begin) - 1,
380                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
381                     start, end, y1, colourOnly);
382             if (drawn)
383             {
384               drawnColour = featureColour;
385             }
386           }
387         }
388       }
389     }
390
391     if (transparency != 1.0f && g != null)
392     {
393       /*
394        * reset transparency
395        */
396       Graphics2D g2 = (Graphics2D) g;
397       g2.setComposite(NO_TRANSPARENCY);
398     }
399
400     return drawnColour;
401   }
402
403   /**
404    * Answers true if the feature belongs to a feature group which is not
405    * currently displayed, else false
406    * 
407    * @param sequenceFeature
408    * @return
409    */
410   protected boolean featureGroupNotShown(
411           final SequenceFeature sequenceFeature)
412   {
413     return featureGroups != null
414             && sequenceFeature.featureGroup != null
415             && sequenceFeature.featureGroup.length() != 0
416             && featureGroups.containsKey(sequenceFeature.featureGroup)
417             && !featureGroups.get(sequenceFeature.featureGroup)
418                     .booleanValue();
419   }
420
421   /**
422    * Called when alignment in associated view has new/modified features to
423    * discover and display.
424    * 
425    */
426   @Override
427   public void featuresAdded()
428   {
429     findAllFeatures();
430   }
431
432   /**
433    * Returns the sequence feature colour rendered at the given sequence
434    * position, or null if none found. The feature of highest render order (i.e.
435    * on top) is found, subject to both feature type and feature group being
436    * visible, and its colour returned.
437    * 
438    * @param seq
439    * @param pos
440    * @return
441    */
442   Color findFeatureColour(SequenceI seq, int pos)
443   {
444     SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
445     if (sequenceFeatures == null || sequenceFeatures.length == 0)
446     {
447       return null;
448     }
449   
450     /*
451      * check for new feature added while processing
452      */
453     updateFeatures();
454
455     /*
456      * inspect features in reverse renderOrder (the last in the array is 
457      * displayed on top) until we find one that is rendered at the position
458      */
459     for (int renderIndex = renderOrder.length - 1; renderIndex >= 0; renderIndex--)
460     {
461       String type = renderOrder[renderIndex];
462       if (!showFeatureOfType(type))
463       {
464         continue;
465       }
466
467       for (int sfindex = 0; sfindex < sequenceFeatures.length; sfindex++)
468       {
469         SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
470         if (!sequenceFeature.type.equals(type))
471         {
472           continue;
473         }
474
475         if (featureGroupNotShown(sequenceFeature))
476         {
477           continue;
478         }
479
480         /*
481          * check the column position is within the feature range
482          * (or is one of the two contact positions for a contact feature)
483          */
484         boolean featureIsAtPosition = sequenceFeature.begin <= pos
485                 && sequenceFeature.end >= pos;
486         if (sequenceFeature.isContactFeature())
487         {
488           featureIsAtPosition = sequenceFeature.begin == pos
489                   || sequenceFeature.end == pos;
490         }
491         if (featureIsAtPosition)
492         {
493           return getColour(sequenceFeature);
494         }
495       }
496     }
497   
498     /*
499      * no displayed feature found at position
500      */
501     return null;
502   }
503 }