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