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