JAL-3210 Improvements to eclipse detection. New src tree and SwingJS updated from...
[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    * 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     char s = '\0';
96     for (int i = featureStart; i <= featureEnd; i++)
97     {
98
99       // colourOnly is just for Overview -- no need to check this again
100
101       if (!colourOnly && Comparison.isGap(s = seq.getCharAt(i)))
102       {
103         continue;
104       }
105
106       g.setColor(featureColour);
107
108       g.fillRect((i - start) * charWidth, y1, charWidth, charHeight);
109
110       if (colourOnly)
111       {
112         return true;
113       }
114
115       if (!validCharWidth)
116       {
117         continue;
118       }
119
120       g.setColor(Color.white);
121       int charOffset = (charWidth - fm.charWidth(s)) / 2;
122       g.drawString(String.valueOf(s),
123               charOffset + (charWidth * (i - start)), pady);
124     }
125     return true;
126   }
127
128   /**
129    * 
130    * BH - this method is never called?
131    * 
132    * Renders the sequence using the given SCORE feature colour between the given
133    * start and end columns. Returns true if at least one column is drawn, else
134    * false (the feature range does not overlap the start and end positions).
135    * 
136    * @param g
137    * @param seq
138    * @param fstart
139    * @param fend
140    * @param featureColour
141    * @param start
142    * @param end
143    * @param y1
144    * @param bs
145    * @param colourOnly
146    * @return
147    */
148   boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
149           int fend, Color featureColour, int start, int end, int y1,
150           byte[] bs, boolean colourOnly)
151   {
152     if (fstart > end || fend < start)
153     {
154       return false;
155     }
156
157     if (fstart < start)
158     { // fix for if the feature we have starts before the sequence start,
159       fstart = start; // but the feature end is still valid!!
160     }
161
162     if (fend >= end)
163     {
164       fend = end;
165     }
166     int charHeight = av.getCharHeight();
167     int pady = (y1 + charHeight) - charHeight / 5;
168     int ystrt = 0, yend = charHeight;
169     if (bs[0] != 0)
170     {
171       // signed - zero is always middle of residue line.
172       if (bs[1] < 128)
173       {
174         yend = charHeight * (128 - bs[1]) / 512;
175         ystrt = charHeight - yend / 2;
176       }
177       else
178       {
179         ystrt = charHeight / 2;
180         yend = charHeight * (bs[1] - 128) / 512;
181       }
182     }
183     else
184     {
185       yend = charHeight * bs[1] / 255;
186       ystrt = charHeight - yend;
187
188     }
189
190     FontMetrics fm = g.getFontMetrics();
191     int charWidth = av.getCharWidth();
192
193     for (int i = fstart; i <= fend; i++)
194     {
195       char s = seq.getCharAt(i);
196
197       if (Comparison.isGap(s))
198       {
199         continue;
200       }
201
202       g.setColor(featureColour);
203       int x = (i - start) * charWidth;
204       g.drawRect(x, y1, charWidth, charHeight);
205       g.fillRect(x, y1 + ystrt, charWidth, yend);
206
207       if (colourOnly || !av.isValidCharWidth())
208       {
209         continue;
210       }
211
212       g.setColor(Color.black);
213       int charOffset = (charWidth - fm.charWidth(s)) / 2;
214       g.drawString(String.valueOf(s),
215               charOffset + (charWidth * (i - start)), pady);
216     }
217     return true;
218   }
219
220   /**
221    * {@inheritDoc}
222    */
223   @Override
224   public Color findFeatureColour(SequenceI seq, int column, Graphics g)
225   {
226     // BH 2019.08.01
227     // this is already checked in FeatureColorFinder
228     // if (!av.isShowSequenceFeatures())
229     // {
230     // return null;
231     // }
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 (may be null only if t == 1 from
270    *          colourOnly==true)
271    * @param seq
272    * @param start
273    *          start column
274    * @param end
275    *          end column
276    * @param y1
277    *          vertical offset at which to draw on the graphics
278    * @param colourOnly
279    *          if true, only do enough to determine the colour for the position,
280    *          do not draw the character
281    * @return
282    */
283   public synchronized Color drawSequence(final Graphics g,
284           final SequenceI seq, int start, int end, int y1,
285           boolean colourOnly)
286   {
287     // from SeqCanvas and OverviewRender
288     /*
289      * if columns are all gapped, or sequence has no features, nothing to do
290      */
291     ContiguousI visiblePositions;
292     if (!seq.getFeatures().hasFeatures() || (visiblePositions = seq
293             .findPositions(start + 1, end + 1)) == null)
294     {
295       return null;
296     }
297
298     int vp0 = visiblePositions.getBegin();
299     int vp1 = visiblePositions.getEnd();
300
301     updateFeatures();
302
303     if (transparency != 1f) // g cannot be null here if trans == 1f - BH // && g
304                             // != null)
305     {
306       ((Graphics2D) g).setComposite(
307               AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
308               transparency));
309     }
310
311     Color drawnColour = null;
312
313     /*
314      * iterate over features in ordering of their rendering (last is on top)
315      */
316     for (int renderIndex = 0, n = renderOrder.length; renderIndex < n; renderIndex++)
317     {
318       String type = renderOrder[renderIndex];
319       if (!seq.hasFeatures(type) || !showFeatureOfType(type))
320       {
321         continue;
322       }
323
324       FeatureColourI fc = getFeatureStyle(type);
325       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(vp0,
326               vp1, type);
327
328       // colourOnly (i.e. Overview) can only be here if translucent, so
329       // there is no need to check for filtering
330       if (!colourOnly && fc.isSimpleColour())
331       {
332         filterFeaturesForDisplay(overlaps);
333       }
334
335       for (int i = overlaps.size(); --i >= 0;)
336       {
337         SequenceFeature sf = overlaps.get(i);
338         Color featureColour = getColor(sf, fc);
339         if (featureColour == null)
340         {
341           /*
342            * feature excluded by visibility settings, filters, or colour threshold
343            */
344           continue;
345         }
346
347         /*
348          * if feature starts/ends outside the visible range,
349          * restrict to visible positions (or if a contact feature,
350          * to a single position)
351          */
352         int sf0 = sf.getBegin();
353         int sf1 = sf.getEnd();
354         int visibleStart = sf0;
355         if (visibleStart < vp0)
356         {
357           visibleStart = sf.isContactFeature() ? sf1 : vp0;
358         }
359         int visibleEnd = sf1;
360         if (visibleEnd > vp1)
361         {
362           visibleEnd = sf.isContactFeature() ? sf0 : vp1;
363         }
364
365         int featureStartCol = seq.findIndex(visibleStart);
366         int featureEndCol = (sf.begin == sf.end ? featureStartCol
367                 : seq.findIndex(visibleEnd));
368
369         // Color featureColour = getColour(sequenceFeature);
370
371         boolean isContactFeature = sf.isContactFeature();
372
373         if (isContactFeature)
374         {
375           boolean drawn = renderFeature(g, seq, featureStartCol - 1,
376                   featureStartCol - 1, featureColour, start, end, y1,
377                   colourOnly);
378           drawn |= renderFeature(g, seq, featureEndCol - 1,
379                   featureEndCol - 1, featureColour, start, end, y1,
380                   colourOnly);
381           if (drawn)
382           {
383             drawnColour = featureColour;
384           }
385         }
386         else
387         {
388           /*
389            * showing feature score by height of colour
390            * is not implemented as a selectable option 
391            *
392           if (av.isShowSequenceFeaturesHeight()
393                   && !Float.isNaN(sequenceFeature.score))
394           {
395             boolean drawn = renderScoreFeature(g, seq,
396                     seq.findIndex(sequenceFeature.begin) - 1,
397                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
398                     start, end, y1, normaliseScore(sequenceFeature),
399                     colourOnly);
400             if (drawn)
401             {
402               drawnColour = featureColour;
403             }
404           }
405           else
406           {
407           */
408           boolean drawn = renderFeature(g, seq, featureStartCol - 1,
409                   featureEndCol - 1, featureColour, start, end, y1,
410                   colourOnly);
411           if (drawn)
412           {
413             drawnColour = featureColour;
414           }
415           /*}*/
416         }
417       }
418     }
419
420     if (transparency != 1.0f)
421     {
422       /*
423        * reset transparency
424        */
425       ((Graphics2D) g).setComposite(NO_TRANSPARENCY);
426     }
427
428     return drawnColour;
429   }
430
431   /**
432    * Called when alignment in associated view has new/modified features to
433    * discover and display.
434    * 
435    */
436   @Override
437   public void featuresAdded()
438   {
439     findAllFeatures();
440   }
441
442   private List<SequenceFeature> overlaps = (Platform.isJS()
443           ? new ArrayList<>()
444           : null);
445
446   /**
447    * Returns the sequence feature colour rendered at the given column position,
448    * or null if none found. The feature of highest render order (i.e. on top) is
449    * found, subject to both feature type and feature group being visible, and
450    * its colour returned. This method is suitable when no feature transparency
451    * applied (only the topmost visible feature colour is rendered).
452    * <p>
453    * Note this method does not check for a gap in the column so would return the
454    * colour for features enclosing a gapped column. Check for gap before calling
455    * if different behaviour is wanted.
456    * 
457    * BH 2019.07.30
458    * 
459    * Adds a result ArrayList to parameters in order to avoid an unnecessary
460    * construction of that for every pixel checked.
461    * 
462    * 
463    * @param seq
464    * @param column
465    *          (1..)
466    * @return
467    */
468   private Color findFeatureColour(SequenceI seq, int column)
469   {
470     /*
471      * check for new feature added while processing
472      */
473     updateFeatures();
474
475     /*
476      * inspect features in reverse renderOrder (the last in the array is 
477      * displayed on top) until we find one that is rendered at the position
478      */
479     for (int renderIndex = renderOrder.length; --renderIndex >= 0;)
480     {
481       String type = renderOrder[renderIndex];
482       if (!seq.hasFeatures(type) || !showFeatureOfType(type))
483       {
484         continue;
485       }
486
487       if (overlaps != null)
488       {
489         overlaps.clear();
490       }
491       List<SequenceFeature> list = seq.findFeatures(column, type, overlaps);
492       if (list.size() > 0)
493       {
494         for (int i = 0, n = list.size(); i < n; i++)
495         {
496           SequenceFeature sf = list.get(i);
497           if (featureGroupNotShown(sf))
498           {
499             continue;
500           }
501           Color col = getColour(sf);
502           if (col != null)
503           {
504             return col;
505           }
506         }
507       }
508     }
509
510     /*
511      * no displayed feature found at position
512      */
513     return null;
514   }
515 }