3989b0c83994adee088325d0cee8b2a3e23757fb
[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.util.ReverseListIterator;
32 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
33
34 import java.awt.AlphaComposite;
35 import java.awt.Color;
36 import java.awt.FontMetrics;
37 import java.awt.Graphics;
38 import java.awt.Graphics2D;
39 import java.util.List;
40
41 public class FeatureRenderer extends FeatureRendererModel
42 {
43   private static final AlphaComposite NO_TRANSPARENCY = AlphaComposite
44           .getInstance(AlphaComposite.SRC_OVER, 1.0f);
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, charHeight);
108
109       if (colourOnly || !validCharWidth)
110       {
111         continue;
112       }
113
114       g.setColor(Color.white);
115       int charOffset = (charWidth - fm.charWidth(s)) / 2;
116       g.drawString(String.valueOf(s),
117               charOffset + (charWidth * (i - start)), pady);
118     }
119     return true;
120   }
121
122   /**
123    * Renders the sequence using the given SCORE feature colour between the given
124    * start and end columns. Returns true if at least one column is drawn, else
125    * false (the feature range does not overlap the start and end positions).
126    * 
127    * @param g
128    * @param seq
129    * @param fstart
130    * @param fend
131    * @param featureColour
132    * @param start
133    * @param end
134    * @param y1
135    * @param bs
136    * @param colourOnly
137    * @return
138    */
139   boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
140           int fend, Color featureColour, int start, int end, int y1,
141           byte[] bs, boolean colourOnly)
142   {
143     if (fstart > end || fend < start)
144     {
145       return false;
146     }
147
148     if (fstart < start)
149     { // fix for if the feature we have starts before the sequence start,
150       fstart = start; // but the feature end is still valid!!
151     }
152
153     if (fend >= end)
154     {
155       fend = end;
156     }
157     int charHeight = av.getCharHeight();
158     int pady = (y1 + charHeight) - charHeight / 5;
159     int ystrt = 0, yend = charHeight;
160     if (bs[0] != 0)
161     {
162       // signed - zero is always middle of residue line.
163       if (bs[1] < 128)
164       {
165         yend = charHeight * (128 - bs[1]) / 512;
166         ystrt = charHeight - yend / 2;
167       }
168       else
169       {
170         ystrt = charHeight / 2;
171         yend = charHeight * (bs[1] - 128) / 512;
172       }
173     }
174     else
175     {
176       yend = charHeight * bs[1] / 255;
177       ystrt = charHeight - yend;
178
179     }
180
181     FontMetrics fm = g.getFontMetrics();
182     int charWidth = av.getCharWidth();
183
184     for (int i = fstart; i <= fend; i++)
185     {
186       char s = seq.getCharAt(i);
187
188       if (Comparison.isGap(s))
189       {
190         continue;
191       }
192
193       g.setColor(featureColour);
194       int x = (i - start) * charWidth;
195       g.drawRect(x, y1, charWidth, charHeight);
196       g.fillRect(x, y1 + ystrt, charWidth, yend);
197
198       if (colourOnly || !av.isValidCharWidth())
199       {
200         continue;
201       }
202
203       g.setColor(Color.black);
204       int charOffset = (charWidth - fm.charWidth(s)) / 2;
205       g.drawString(String.valueOf(s),
206               charOffset + (charWidth * (i - start)), pady);
207     }
208     return true;
209   }
210
211   /**
212    * {@inheritDoc}
213    */
214   @Override
215   public Color findFeatureColour(SequenceI seq, int column, Graphics g)
216   {
217     if (!av.isShowSequenceFeatures())
218     {
219       return null;
220     }
221
222     // column is 'base 1' but getCharAt is an array index (ie from 0)
223     if (Comparison.isGap(seq.getCharAt(column - 1)))
224     {
225       /*
226        * returning null allows the colour scheme to provide gap colour
227        * - normally white, but can be customised
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, 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     /*
276      * if columns are all gapped, or sequence has no features, nothing to do
277      */
278     Range visiblePositions = seq.findPositions(start+1, end+1);
279     if (visiblePositions == null || (!seq.getFeatures().hasFeatures()
280             && !av.isShowComplementFeatures()))
281     {
282       return null;
283     }
284
285     updateFeatures();
286
287     if (transparency != 1f && g != null)
288     {
289       Graphics2D g2 = (Graphics2D) g;
290       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
291               transparency));
292     }
293
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       FeatureColourI fc = getFeatureStyle(type);
308       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
309               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
310
311       if (fc.isSimpleColour())
312       {
313         filterFeaturesForDisplay(overlaps);
314       }
315
316       for (SequenceFeature sf : overlaps)
317       {
318         Color featureColour = getColor(sf, fc);
319         if (featureColour == null)
320         {
321           /*
322            * feature excluded by filters, or colour threshold
323            */
324           continue;
325         }
326
327         /*
328          * if feature starts/ends outside the visible range,
329          * restrict to visible positions (or if a contact feature,
330          * to a single position)
331          */
332         int visibleStart = sf.getBegin();
333         if (visibleStart < visiblePositions.getBegin())
334         {
335           visibleStart = sf.isContactFeature() ? sf.getEnd()
336                   : visiblePositions.getBegin();
337         }
338         int visibleEnd = sf.getEnd();
339         if (visibleEnd > visiblePositions.getEnd())
340         {
341           visibleEnd = sf.isContactFeature() ? sf.getBegin()
342                   : visiblePositions.getEnd();
343         }
344
345         int featureStartCol = seq.findIndex(visibleStart);
346         int featureEndCol = sf.begin == sf.end ? featureStartCol : seq
347                 .findIndex(visibleEnd);
348
349         // Color featureColour = getColour(sequenceFeature);
350
351         boolean isContactFeature = sf.isContactFeature();
352
353         if (isContactFeature)
354         {
355           boolean drawn = renderFeature(g, seq, featureStartCol - 1,
356                   featureStartCol - 1, featureColour, start, end, y1,
357                   colourOnly);
358           drawn |= renderFeature(g, seq, featureEndCol - 1,
359                   featureEndCol - 1, featureColour, start, end, y1,
360                   colourOnly);
361           if (drawn)
362           {
363             drawnColour = featureColour;
364           }
365         }
366         else
367         {
368           /*
369            * showing feature score by height of colour
370            * is not implemented as a selectable option 
371            *
372           if (av.isShowSequenceFeaturesHeight()
373                   && !Float.isNaN(sequenceFeature.score))
374           {
375             boolean drawn = renderScoreFeature(g, seq,
376                     seq.findIndex(sequenceFeature.begin) - 1,
377                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
378                     start, end, y1, normaliseScore(sequenceFeature),
379                     colourOnly);
380             if (drawn)
381             {
382               drawnColour = featureColour;
383             }
384           }
385           else
386           {
387           */
388             boolean drawn = renderFeature(g, seq,
389                     featureStartCol - 1,
390                     featureEndCol - 1, featureColour,
391                     start, end, y1, colourOnly);
392             if (drawn)
393             {
394               drawnColour = featureColour;
395             }
396           /*}*/
397         }
398       }
399     }
400
401     /*
402      * if configured to do so, find and show complement's features
403      */
404     if (av.isShowComplementFeatures())
405     {
406       AlignViewportI comp = av.getCodingComplement();
407       FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp)
408               .getFeatureRenderer();
409       for (int pos = visiblePositions.start; pos <= visiblePositions.end; pos++)
410       {
411         int column = seq.findIndex(pos);
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       ReverseListIterator<SequenceFeature> it = new ReverseListIterator<>(
482               features);
483       while (it.hasNext())
484       {
485         SequenceFeature sf = it.next();
486         if (!fr2.featureGroupNotShown(sf))
487         {
488           Color col = fr2.getColour(sf);
489           if (col != null)
490           {
491             return col;
492           }
493         }
494       }
495     }
496
497     /*
498      * inspect features in reverse renderOrder (the last in the array is 
499      * displayed on top) until we find one that is rendered at the position
500      */
501     for (int renderIndex = renderOrder.length
502             - 1; renderIndex >= 0; renderIndex--)
503     {
504       String type = renderOrder[renderIndex];
505       if (!showFeatureOfType(type))
506       {
507         continue;
508       }
509
510       List<SequenceFeature> overlaps = seq.findFeatures(column, column,
511               type);
512       for (SequenceFeature sequenceFeature : overlaps)
513       {
514         if (!featureGroupNotShown(sequenceFeature))
515         {
516           Color col = getColour(sequenceFeature);
517           if (col != null)
518           {
519             return col;
520           }
521         }
522       }
523     }
524
525     /*
526      * no displayed feature found at position
527      */
528     return null;
529   }
530 }