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