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