JAL-3438 spotless for 2.11.2.0
[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 java.awt.AlphaComposite;
24 import java.awt.Color;
25 import java.awt.FontMetrics;
26 import java.awt.Graphics;
27 import java.awt.Graphics2D;
28 import java.util.List;
29
30 import jalview.api.AlignViewportI;
31 import jalview.api.FeatureColourI;
32 import jalview.datamodel.ContiguousI;
33 import jalview.datamodel.MappedFeatures;
34 import jalview.datamodel.SequenceFeature;
35 import jalview.datamodel.SequenceI;
36 import jalview.gui.AlignFrame;
37 import jalview.gui.Desktop;
38 import jalview.util.Comparison;
39 import jalview.util.ReverseListIterator;
40 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
41
42 public class FeatureRenderer extends FeatureRendererModel
43 {
44   private static final AlphaComposite NO_TRANSPARENCY = AlphaComposite
45           .getInstance(AlphaComposite.SRC_OVER, 1.0f);
46
47   /**
48    * Constructor given a viewport
49    * 
50    * @param viewport
51    */
52   public FeatureRenderer(AlignViewportI viewport)
53   {
54     this.av = viewport;
55   }
56
57   /**
58    * Renders the sequence using the given feature colour between the given start
59    * and end columns. Returns true if at least one column is drawn, else false
60    * (the feature range does not overlap the start and end positions).
61    * 
62    * @param g
63    * @param seq
64    * @param featureStart
65    * @param featureEnd
66    * @param featureColour
67    * @param start
68    * @param end
69    * @param y1
70    * @param colourOnly
71    * @return
72    */
73   boolean renderFeature(Graphics g, SequenceI seq, int featureStart,
74           int featureEnd, Color featureColour, int start, int end, int y1,
75           boolean colourOnly)
76   {
77     int charHeight = av.getCharHeight();
78     int charWidth = av.getCharWidth();
79     boolean validCharWidth = av.isValidCharWidth();
80
81     if (featureStart > end || featureEnd < start)
82     {
83       return false;
84     }
85
86     if (featureStart < start)
87     {
88       featureStart = start;
89     }
90     if (featureEnd >= end)
91     {
92       featureEnd = end;
93     }
94     int pady = (y1 + charHeight) - charHeight / 5;
95
96     FontMetrics fm = g.getFontMetrics();
97     for (int i = featureStart; i <= featureEnd; i++)
98     {
99       char s = seq.getCharAt(i);
100
101       if (Comparison.isGap(s))
102       {
103         continue;
104       }
105
106       g.setColor(featureColour);
107
108       g.fillRect((i - start) * charWidth, y1, charWidth, charHeight);
109
110       if (colourOnly || !validCharWidth)
111       {
112         continue;
113       }
114
115       /*
116        * JAL-3045 text is always drawn over features, even if
117        * 'Show Text' is unchecked in the format menu
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    * Renders the sequence using the given SCORE feature colour between the given
129    * start and end columns. Returns true if at least one column is drawn, else
130    * false (the feature range does not overlap the start and end positions).
131    * 
132    * @param g
133    * @param seq
134    * @param fstart
135    * @param fend
136    * @param featureColour
137    * @param start
138    * @param end
139    * @param y1
140    * @param bs
141    * @param colourOnly
142    * @return
143    */
144   boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
145           int fend, Color featureColour, int start, int end, int y1,
146           byte[] bs, boolean colourOnly)
147   {
148     if (fstart > end || fend < start)
149     {
150       return false;
151     }
152
153     if (fstart < start)
154     { // fix for if the feature we have starts before the sequence start,
155       fstart = start; // but the feature end is still valid!!
156     }
157
158     if (fend >= end)
159     {
160       fend = end;
161     }
162     int charHeight = av.getCharHeight();
163     int pady = (y1 + charHeight) - charHeight / 5;
164     int ystrt = 0, yend = charHeight;
165     if (bs[0] != 0)
166     {
167       // signed - zero is always middle of residue line.
168       if (bs[1] < 128)
169       {
170         yend = charHeight * (128 - bs[1]) / 512;
171         ystrt = charHeight - yend / 2;
172       }
173       else
174       {
175         ystrt = charHeight / 2;
176         yend = charHeight * (bs[1] - 128) / 512;
177       }
178     }
179     else
180     {
181       yend = charHeight * bs[1] / 255;
182       ystrt = charHeight - yend;
183
184     }
185
186     FontMetrics fm = g.getFontMetrics();
187     int charWidth = av.getCharWidth();
188
189     for (int i = fstart; i <= fend; i++)
190     {
191       char s = seq.getCharAt(i);
192
193       if (Comparison.isGap(s))
194       {
195         continue;
196       }
197
198       g.setColor(featureColour);
199       int x = (i - start) * charWidth;
200       g.drawRect(x, y1, charWidth, charHeight);
201       g.fillRect(x, y1 + ystrt, charWidth, yend);
202
203       if (colourOnly || !av.isValidCharWidth())
204       {
205         continue;
206       }
207
208       g.setColor(Color.black);
209       int charOffset = (charWidth - fm.charWidth(s)) / 2;
210       g.drawString(String.valueOf(s),
211               charOffset + (charWidth * (i - start)), pady);
212     }
213     return true;
214   }
215
216   /**
217    * {@inheritDoc}
218    */
219   @Override
220   public Color findFeatureColour(SequenceI seq, int column, Graphics g)
221   {
222     if (!av.isShowSequenceFeatures())
223     {
224       return null;
225     }
226
227     // column is 'base 1' but getCharAt is an array index (ie from 0)
228     if (Comparison.isGap(seq.getCharAt(column - 1)))
229     {
230       /*
231        * returning null allows the colour scheme to provide gap colour
232        * - normally white, but can be customised
233        */
234       return null;
235     }
236
237     Color renderedColour = null;
238     if (transparency == 1.0f)
239     {
240       /*
241        * simple case - just find the topmost rendered visible feature colour
242        */
243       renderedColour = findFeatureColour(seq, column);
244     }
245     else
246     {
247       /*
248        * transparency case - draw all visible features in render order to
249        * build up a composite colour on the graphics context
250        */
251       renderedColour = drawSequence(g, seq, column, column, 0, true);
252     }
253     return renderedColour;
254   }
255
256   /**
257    * Draws the sequence features on the graphics context, or just determines the
258    * colour that would be drawn (if flag colourOnly is true). Returns the last
259    * colour drawn (which may not be the effective colour if transparency
260    * applies), or null if no feature is drawn in the range given.
261    * 
262    * @param g
263    *          the graphics context to draw on (may be null if colourOnly==true)
264    * @param seq
265    * @param start
266    *          start column
267    * @param end
268    *          end column
269    * @param y1
270    *          vertical offset at which to draw on the graphics
271    * @param colourOnly
272    *          if true, only do enough to determine the colour for the position,
273    *          do not draw the character
274    * @return
275    */
276   public synchronized Color drawSequence(final Graphics g,
277           final SequenceI seq, int start, int end, int y1,
278           boolean colourOnly)
279   {
280     /*
281      * if columns are all gapped, or sequence has no features, nothing to do
282      */
283     ContiguousI visiblePositions = seq.findPositions(start + 1, end + 1);
284     if (visiblePositions == null || !seq.getFeatures().hasFeatures()
285             && !av.isShowComplementFeatures())
286     {
287       return null;
288     }
289
290     updateFeatures();
291
292     if (transparency != 1f && g != null)
293     {
294       Graphics2D g2 = (Graphics2D) g;
295       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
296               transparency));
297     }
298
299     Color drawnColour = null;
300
301     /*
302      * draw 'complement' features below ours if configured to do so
303      */
304     if (av.isShowComplementFeatures()
305             && !av.isShowComplementFeaturesOnTop())
306     {
307       drawnColour = drawComplementFeatures(g, seq, start, end, y1,
308               colourOnly, visiblePositions, drawnColour);
309     }
310
311     /*
312      * iterate over features in ordering of their rendering (last is on top)
313      */
314     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
315     {
316       String type = renderOrder[renderIndex];
317       if (!showFeatureOfType(type))
318       {
319         continue;
320       }
321
322       FeatureColourI fc = getFeatureStyle(type);
323       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
324               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
325
326       if (overlaps.size() > 1 && fc.isSimpleColour())
327       {
328         filterFeaturesForDisplay(overlaps);
329       }
330
331       for (SequenceFeature sf : overlaps)
332       {
333         Color featureColour = getColor(sf, fc);
334         if (featureColour == null)
335         {
336           /*
337            * feature excluded by filters, or colour threshold
338            */
339           continue;
340         }
341
342         /*
343          * if feature starts/ends outside the visible range,
344          * restrict to visible positions (or if a contact feature,
345          * to a single position)
346          */
347         int visibleStart = sf.getBegin();
348         if (visibleStart < visiblePositions.getBegin())
349         {
350           visibleStart = sf.isContactFeature() ? sf.getEnd()
351                   : visiblePositions.getBegin();
352         }
353         int visibleEnd = sf.getEnd();
354         if (visibleEnd > visiblePositions.getEnd())
355         {
356           visibleEnd = sf.isContactFeature() ? sf.getBegin()
357                   : visiblePositions.getEnd();
358         }
359
360         int featureStartCol = seq.findIndex(visibleStart);
361         int featureEndCol = sf.begin == sf.end ? featureStartCol
362                 : seq.findIndex(visibleEnd);
363
364         // Color featureColour = getColour(sequenceFeature);
365
366         boolean isContactFeature = sf.isContactFeature();
367
368         if (isContactFeature)
369         {
370           boolean drawn = renderFeature(g, seq, featureStartCol - 1,
371                   featureStartCol - 1, featureColour, start, end, y1,
372                   colourOnly);
373           drawn |= renderFeature(g, seq, featureEndCol - 1,
374                   featureEndCol - 1, featureColour, start, end, y1,
375                   colourOnly);
376           if (drawn)
377           {
378             drawnColour = featureColour;
379           }
380         }
381         else
382         {
383           /*
384            * showing feature score by height of colour
385            * is not implemented as a selectable option 
386            *
387           if (av.isShowSequenceFeaturesHeight()
388                   && !Float.isNaN(sequenceFeature.score))
389           {
390             boolean drawn = renderScoreFeature(g, seq,
391                     seq.findIndex(sequenceFeature.begin) - 1,
392                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
393                     start, end, y1, normaliseScore(sequenceFeature),
394                     colourOnly);
395             if (drawn)
396             {
397               drawnColour = featureColour;
398             }
399           }
400           else
401           {
402           */
403           boolean drawn = renderFeature(g, seq, featureStartCol - 1,
404                   featureEndCol - 1, featureColour, start, end, y1,
405                   colourOnly);
406           if (drawn)
407           {
408             drawnColour = featureColour;
409           }
410           /*}*/
411         }
412       }
413     }
414
415     /*
416      * draw 'complement' features above ours if configured to do so
417      */
418     if (av.isShowComplementFeatures() && av.isShowComplementFeaturesOnTop())
419     {
420       drawnColour = drawComplementFeatures(g, seq, start, end, y1,
421               colourOnly, visiblePositions, drawnColour);
422     }
423
424     if (transparency != 1.0f && g != null)
425     {
426       /*
427        * reset transparency
428        */
429       Graphics2D g2 = (Graphics2D) g;
430       g2.setComposite(NO_TRANSPARENCY);
431     }
432
433     return drawnColour;
434   }
435
436   /**
437    * Find any features on the CDS/protein complement of the sequence region and
438    * draw them, with visibility and colouring as configured in the complementary
439    * viewport
440    * 
441    * @param g
442    * @param seq
443    * @param start
444    * @param end
445    * @param y1
446    * @param colourOnly
447    * @param visiblePositions
448    * @param drawnColour
449    * @return
450    */
451   Color drawComplementFeatures(final Graphics g, final SequenceI seq,
452           int start, int end, int y1, boolean colourOnly,
453           ContiguousI visiblePositions, Color drawnColour)
454   {
455     AlignViewportI comp = av.getCodingComplement();
456     FeatureRenderer fr2 = Desktop.getAlignFrameFor(comp)
457             .getFeatureRenderer();
458
459     final int visibleStart = visiblePositions.getBegin();
460     final int visibleEnd = visiblePositions.getEnd();
461
462     for (int pos = visibleStart; pos <= visibleEnd; pos++)
463     {
464       int column = seq.findIndex(pos);
465       MappedFeatures mf = fr2.findComplementFeaturesAtResidue(seq, pos);
466       if (mf != null)
467       {
468         for (SequenceFeature sf : mf.features)
469         {
470           FeatureColourI fc = fr2.getFeatureStyle(sf.getType());
471           Color featureColour = fr2.getColor(sf, fc);
472           renderFeature(g, seq, column - 1, column - 1, featureColour,
473                   start, end, y1, colourOnly);
474           drawnColour = featureColour;
475         }
476       }
477     }
478     return drawnColour;
479   }
480
481   /**
482    * Called when alignment in associated view has new/modified features to
483    * discover and display.
484    * 
485    */
486   @Override
487   public void featuresAdded()
488   {
489     findAllFeatures();
490   }
491
492   /**
493    * Returns the sequence feature colour rendered at the given column position,
494    * or null if none found. The feature of highest render order (i.e. on top) is
495    * found, subject to both feature type and feature group being visible, and
496    * its colour returned. This method is suitable when no feature transparency
497    * applied (only the topmost visible feature colour is rendered).
498    * <p>
499    * Note this method does not check for a gap in the column so would return the
500    * colour for features enclosing a gapped column. Check for gap before calling
501    * if different behaviour is wanted.
502    * 
503    * @param seq
504    * @param column
505    *          (1..)
506    * @return
507    */
508   Color findFeatureColour(SequenceI seq, int column)
509   {
510     /*
511      * check for new feature added while processing
512      */
513     updateFeatures();
514
515     /*
516      * show complement features on top (if configured to show them)
517      */
518     if (av.isShowComplementFeatures() && av.isShowComplementFeaturesOnTop())
519     {
520       Color col = findComplementFeatureColour(seq, column);
521       if (col != null)
522       {
523         return col;
524       }
525     }
526
527     /*
528      * inspect features in reverse renderOrder (the last in the array is 
529      * displayed on top) until we find one that is rendered at the position
530      */
531     for (int renderIndex = renderOrder.length
532             - 1; renderIndex >= 0; renderIndex--)
533     {
534       String type = renderOrder[renderIndex];
535       if (!showFeatureOfType(type))
536       {
537         continue;
538       }
539
540       List<SequenceFeature> overlaps = seq.findFeatures(column, column,
541               type);
542       for (SequenceFeature sequenceFeature : overlaps)
543       {
544         if (!featureGroupNotShown(sequenceFeature))
545         {
546           Color col = getColour(sequenceFeature);
547           if (col != null)
548           {
549             return col;
550           }
551         }
552       }
553     }
554
555     /*
556      * show complement features underneath (if configured to show them)
557      */
558     Color col = null;
559     if (av.isShowComplementFeatures()
560             && !av.isShowComplementFeaturesOnTop())
561     {
562       col = findComplementFeatureColour(seq, column);
563     }
564
565     return col;
566   }
567
568   Color findComplementFeatureColour(SequenceI seq, int column)
569   {
570     AlignViewportI complement = av.getCodingComplement();
571     AlignFrame af = Desktop.getAlignFrameFor(complement);
572     FeatureRendererModel fr2 = af.getFeatureRenderer();
573     MappedFeatures mf = fr2.findComplementFeaturesAtResidue(seq,
574             seq.findPosition(column - 1));
575     if (mf == null)
576     {
577       return null;
578     }
579     ReverseListIterator<SequenceFeature> it = new ReverseListIterator<>(
580             mf.features);
581     while (it.hasNext())
582     {
583       SequenceFeature sf = it.next();
584       if (!fr2.featureGroupNotShown(sf))
585       {
586         Color col = fr2.getColour(sf);
587         if (col != null)
588         {
589           return col;
590         }
591       }
592     }
593     return null;
594   }
595 }