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