JAL-3383 get color as int options removed (to separate branch 3443)
[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       if (!colourOnly && Comparison.isGap(s = seq.getCharAt(i)))
106       {
107         continue;
108       }
109
110       g.setColor(featureColour);
111
112       g.fillRect((i - start) * charWidth, y1, charWidth, charHeight);
113
114       if (colourOnly)
115       {
116         return true;
117       }
118
119       if (!validCharWidth)
120       {
121         continue;
122       }
123
124       g.setColor(Color.white);
125       int charOffset = (charWidth - fm.charWidth(s)) / 2;
126       g.drawString(String.valueOf(s),
127               charOffset + (charWidth * (i - start)), pady);
128     }
129     return true;
130   }
131
132   /**
133    * 
134    * BH - this method is never called?
135    * 
136    * Renders the sequence using the given SCORE feature colour between the given
137    * start and end columns. Returns true if at least one column is drawn, else
138    * false (the feature range does not overlap the start and end positions).
139    * 
140    * @param g
141    * @param seq
142    * @param fstart
143    * @param fend
144    * @param featureColour
145    * @param start
146    * @param end
147    * @param y1
148    * @param bs
149    * @param colourOnly
150    * @return
151    */
152   boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
153           int fend, Color featureColour, int start, int end, int y1,
154           byte[] bs, boolean colourOnly)
155   {
156     if (fstart > end || fend < start)
157     {
158       return false;
159     }
160
161     if (fstart < start)
162     { // fix for if the feature we have starts before the sequence start,
163       fstart = start; // but the feature end is still valid!!
164     }
165
166     if (fend >= end)
167     {
168       fend = end;
169     }
170     int charHeight = av.getCharHeight();
171     int pady = (y1 + charHeight) - charHeight / 5;
172     int ystrt = 0, yend = charHeight;
173     if (bs[0] != 0)
174     {
175       // signed - zero is always middle of residue line.
176       if (bs[1] < 128)
177       {
178         yend = charHeight * (128 - bs[1]) / 512;
179         ystrt = charHeight - yend / 2;
180       }
181       else
182       {
183         ystrt = charHeight / 2;
184         yend = charHeight * (bs[1] - 128) / 512;
185       }
186     }
187     else
188     {
189       yend = charHeight * bs[1] / 255;
190       ystrt = charHeight - yend;
191
192     }
193
194     FontMetrics fm = g.getFontMetrics();
195     int charWidth = av.getCharWidth();
196
197     for (int i = fstart; i <= fend; i++)
198     {
199       char s = seq.getCharAt(i);
200
201       if (Comparison.isGap(s))
202       {
203         continue;
204       }
205
206       g.setColor(featureColour);
207       int x = (i - start) * charWidth;
208       g.drawRect(x, y1, charWidth, charHeight);
209       g.fillRect(x, y1 + ystrt, charWidth, yend);
210
211       if (colourOnly || !av.isValidCharWidth())
212       {
213         continue;
214       }
215
216       g.setColor(Color.black);
217       int charOffset = (charWidth - fm.charWidth(s)) / 2;
218       g.drawString(String.valueOf(s),
219               charOffset + (charWidth * (i - start)), pady);
220     }
221     return true;
222   }
223
224   /**
225    * {@inheritDoc}
226    */
227   @Override
228   public Color findFeatureColour(SequenceI seq, int column, Graphics g)
229   {
230     // column is 'base 1' but getCharAt is an array index (ie from 0)
231     if (Comparison.isGap(seq.getCharAt(column - 1)))
232     {
233       /*
234        * returning null allows the colour scheme to provide gap colour
235        * - normally white, but can be customised
236        */
237       return null;
238     }
239
240     Color renderedColour = null;
241     if (transparency == 1.0f)
242     {
243       /*
244        * simple case - just find the topmost rendered visible feature colour
245        */
246       renderedColour = findFeatureColour(seq, column);
247     }
248     else
249     {
250       /*
251        * transparency case - draw all visible features in render order to
252        * build up a composite colour on the graphics context
253        */
254       renderedColour = drawSequence(g, seq, column, column, 0, true);
255     }
256     return renderedColour;
257   }
258
259   /**
260    * Draws the sequence features on the graphics context, or just determines the
261    * colour that would be drawn (if flag colourOnly is true). Returns the last
262    * colour drawn (which may not be the effective colour if transparency
263    * applies), or null if no feature is drawn in the range given.
264    * 
265    * @param g
266    *          the graphics context to draw on (null if no transparency applies)
267    * @param seq
268    * @param start
269    *          start column
270    * @param end
271    *          end column
272    * @param y1
273    *          vertical offset at which to draw on the graphics
274    * @param colourOnly
275    *          if true, only do enough to determine the colour for the position,
276    *          do not draw the character
277    * @return
278    */
279   public synchronized Color drawSequence(final Graphics g,
280           final SequenceI seq, int start, int end, int y1,
281           boolean colourOnly)
282   {
283     /*
284      * if columns are all gapped, or sequence has no features, nothing to do
285      */
286     ContiguousI visiblePositions;
287     if (!seq.getFeatures().hasFeatures() || (visiblePositions = seq
288             .findPositions(start + 1, end + 1)) == null)
289     {
290       return null;
291     }
292
293     int vp0 = visiblePositions.getBegin();
294     int vp1 = visiblePositions.getEnd();
295
296     updateFeatures();
297
298     if (transparency != 1f)
299     {
300       Graphics2D g2 = (Graphics2D) g;
301       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
302               transparency));
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 = 0, j = overlaps.size(); i < j; i++)
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 }