JAL-3187 (hacked up) show complement features on structure/Overview
[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     {
280       return null;
281     }
282
283     updateFeatures();
284
285     if (transparency != 1f && g != null)
286     {
287       Graphics2D g2 = (Graphics2D) g;
288       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
289               transparency));
290     }
291
292     Color drawnColour = null;
293
294     /*
295      * iterate over features in ordering of their rendering (last is on top)
296      */
297     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
298     {
299       String type = renderOrder[renderIndex];
300       if (!showFeatureOfType(type))
301       {
302         continue;
303       }
304
305       FeatureColourI fc = getFeatureStyle(type);
306       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
307               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
308
309       if (fc.isSimpleColour())
310       {
311         filterFeaturesForDisplay(overlaps);
312       }
313
314       for (SequenceFeature sf : overlaps)
315       {
316         Color featureColour = getColor(sf, fc);
317         if (featureColour == null)
318         {
319           /*
320            * feature excluded by visibility settings, filters, or colour threshold
321            */
322           continue;
323         }
324
325         /*
326          * if feature starts/ends outside the visible range,
327          * restrict to visible positions (or if a contact feature,
328          * to a single position)
329          */
330         int visibleStart = sf.getBegin();
331         if (visibleStart < visiblePositions.getBegin())
332         {
333           visibleStart = sf.isContactFeature() ? sf.getEnd()
334                   : visiblePositions.getBegin();
335         }
336         int visibleEnd = sf.getEnd();
337         if (visibleEnd > visiblePositions.getEnd())
338         {
339           visibleEnd = sf.isContactFeature() ? sf.getBegin()
340                   : visiblePositions.getEnd();
341         }
342
343         int featureStartCol = seq.findIndex(visibleStart);
344         int featureEndCol = sf.begin == sf.end ? featureStartCol : seq
345                 .findIndex(visibleEnd);
346
347         // Color featureColour = getColour(sequenceFeature);
348
349         boolean isContactFeature = sf.isContactFeature();
350
351         if (isContactFeature)
352         {
353           boolean drawn = renderFeature(g, seq, featureStartCol - 1,
354                   featureStartCol - 1, featureColour, start, end, y1,
355                   colourOnly);
356           drawn |= renderFeature(g, seq, featureEndCol - 1,
357                   featureEndCol - 1, featureColour, start, end, y1,
358                   colourOnly);
359           if (drawn)
360           {
361             drawnColour = featureColour;
362           }
363         }
364         else
365         {
366           /*
367            * showing feature score by height of colour
368            * is not implemented as a selectable option 
369            *
370           if (av.isShowSequenceFeaturesHeight()
371                   && !Float.isNaN(sequenceFeature.score))
372           {
373             boolean drawn = renderScoreFeature(g, seq,
374                     seq.findIndex(sequenceFeature.begin) - 1,
375                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
376                     start, end, y1, normaliseScore(sequenceFeature),
377                     colourOnly);
378             if (drawn)
379             {
380               drawnColour = featureColour;
381             }
382           }
383           else
384           {
385           */
386             boolean drawn = renderFeature(g, seq,
387                     featureStartCol - 1,
388                     featureEndCol - 1, featureColour,
389                     start, end, y1, colourOnly);
390             if (drawn)
391             {
392               drawnColour = featureColour;
393             }
394           /*}*/
395         }
396       }
397     }
398
399     if (transparency != 1.0f && g != null)
400     {
401       /*
402        * reset transparency
403        */
404       Graphics2D g2 = (Graphics2D) g;
405       g2.setComposite(NO_TRANSPARENCY);
406     }
407
408     return drawnColour;
409   }
410
411   /**
412    * Called when alignment in associated view has new/modified features to
413    * discover and display.
414    * 
415    */
416   @Override
417   public void featuresAdded()
418   {
419     findAllFeatures();
420   }
421
422   /**
423    * Returns the sequence feature colour rendered at the given column position,
424    * or null if none found. The feature of highest render order (i.e. on top) is
425    * found, subject to both feature type and feature group being visible, and
426    * its colour returned. This method is suitable when no feature transparency
427    * applied (only the topmost visible feature colour is rendered).
428    * <p>
429    * Note this method does not check for a gap in the column so would return the
430    * colour for features enclosing a gapped column. Check for gap before calling
431    * if different behaviour is wanted.
432    * 
433    * @param seq
434    * @param column
435    *          (1..)
436    * @return
437    */
438   Color findFeatureColour(SequenceI seq, int column)
439   {
440     /*
441      * check for new feature added while processing
442      */
443     updateFeatures();
444
445     /*
446      * show complement features on top (if configured to show them)
447      */
448     if (av.isShowComplementFeatures())
449     {
450       AlignViewportI complement = av.getCodingComplement();
451       AlignFrame af = Desktop.getAlignFrameFor(complement);
452       FeatureRendererModel fr2 = af.getFeatureRenderer();
453       List<SequenceFeature> features = fr2.findComplementFeaturesAtResidue(
454               seq, seq.findPosition(column));
455       // todo: ensure ordered by feature render order
456       for (SequenceFeature sf : features)
457       {
458         if (!fr2.featureGroupNotShown(sf))
459         {
460           Color col = fr2.getColour(sf);
461           if (col != null)
462           {
463             return col;
464           }
465         }
466       }
467     }
468
469     /*
470      * inspect features in reverse renderOrder (the last in the array is 
471      * displayed on top) until we find one that is rendered at the position
472      */
473     for (int renderIndex = renderOrder.length
474             - 1; renderIndex >= 0; renderIndex--)
475     {
476       String type = renderOrder[renderIndex];
477       if (!showFeatureOfType(type))
478       {
479         continue;
480       }
481
482       List<SequenceFeature> overlaps = seq.findFeatures(column, column,
483               type);
484       for (SequenceFeature sequenceFeature : overlaps)
485       {
486         if (!featureGroupNotShown(sequenceFeature))
487         {
488           Color col = getColour(sequenceFeature);
489           if (col != null)
490           {
491             return col;
492           }
493         }
494       }
495     }
496
497     /*
498      * no displayed feature found at position
499      */
500     return null;
501   }
502 }