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