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