JAL-3596 don't draw text over features if Show Text is unselected
[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       if (av.getShowText())
116       {
117         g.setColor(Color.white);
118         int charOffset = (charWidth - fm.charWidth(s)) / 2;
119         g.drawString(String.valueOf(s),
120                 charOffset + (charWidth * (i - start)), pady);
121       }
122     }
123     return true;
124   }
125
126   /**
127    * Renders the sequence using the given SCORE feature colour between the given
128    * start and end columns. Returns true if at least one column is drawn, else
129    * false (the feature range does not overlap the start and end positions).
130    * 
131    * @param g
132    * @param seq
133    * @param fstart
134    * @param fend
135    * @param featureColour
136    * @param start
137    * @param end
138    * @param y1
139    * @param bs
140    * @param colourOnly
141    * @return
142    */
143   boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
144           int fend, Color featureColour, int start, int end, int y1,
145           byte[] bs, boolean colourOnly)
146   {
147     if (fstart > end || fend < start)
148     {
149       return false;
150     }
151
152     if (fstart < start)
153     { // fix for if the feature we have starts before the sequence start,
154       fstart = start; // but the feature end is still valid!!
155     }
156
157     if (fend >= end)
158     {
159       fend = end;
160     }
161     int charHeight = av.getCharHeight();
162     int pady = (y1 + charHeight) - charHeight / 5;
163     int ystrt = 0, yend = charHeight;
164     if (bs[0] != 0)
165     {
166       // signed - zero is always middle of residue line.
167       if (bs[1] < 128)
168       {
169         yend = charHeight * (128 - bs[1]) / 512;
170         ystrt = charHeight - yend / 2;
171       }
172       else
173       {
174         ystrt = charHeight / 2;
175         yend = charHeight * (bs[1] - 128) / 512;
176       }
177     }
178     else
179     {
180       yend = charHeight * bs[1] / 255;
181       ystrt = charHeight - yend;
182
183     }
184
185     FontMetrics fm = g.getFontMetrics();
186     int charWidth = av.getCharWidth();
187
188     for (int i = fstart; i <= fend; i++)
189     {
190       char s = seq.getCharAt(i);
191
192       if (Comparison.isGap(s))
193       {
194         continue;
195       }
196
197       g.setColor(featureColour);
198       int x = (i - start) * charWidth;
199       g.drawRect(x, y1, charWidth, charHeight);
200       g.fillRect(x, y1 + ystrt, charWidth, yend);
201
202       if (colourOnly || !av.isValidCharWidth())
203       {
204         continue;
205       }
206
207       g.setColor(Color.black);
208       int charOffset = (charWidth - fm.charWidth(s)) / 2;
209       g.drawString(String.valueOf(s),
210               charOffset + (charWidth * (i - start)), pady);
211     }
212     return true;
213   }
214
215   /**
216    * {@inheritDoc}
217    */
218   @Override
219   public Color findFeatureColour(SequenceI seq, int column, Graphics g)
220   {
221     if (!av.isShowSequenceFeatures())
222     {
223       return null;
224     }
225
226     // column is 'base 1' but getCharAt is an array index (ie from 0)
227     if (Comparison.isGap(seq.getCharAt(column - 1)))
228     {
229       /*
230        * returning null allows the colour scheme to provide gap colour
231        * - normally white, but can be customised
232        */
233       return null;
234     }
235
236     Color renderedColour = null;
237     if (transparency == 1.0f)
238     {
239       /*
240        * simple case - just find the topmost rendered visible feature colour
241        */
242       renderedColour = findFeatureColour(seq, column);
243     }
244     else
245     {
246       /*
247        * transparency case - draw all visible features in render order to
248        * build up a composite colour on the graphics context
249        */
250       renderedColour = drawSequence(g, seq, column, column, 0, true);
251     }
252     return renderedColour;
253   }
254
255   /**
256    * Draws the sequence features on the graphics context, or just determines the
257    * colour that would be drawn (if flag colourOnly is true). Returns the last
258    * colour drawn (which may not be the effective colour if transparency
259    * applies), or null if no feature is drawn in the range given.
260    * 
261    * @param g
262    *          the graphics context to draw on (may be null if colourOnly==true)
263    * @param seq
264    * @param start
265    *          start column
266    * @param end
267    *          end column
268    * @param y1
269    *          vertical offset at which to draw on the graphics
270    * @param colourOnly
271    *          if true, only do enough to determine the colour for the position,
272    *          do not draw the character
273    * @return
274    */
275   public synchronized Color drawSequence(final Graphics g,
276           final SequenceI seq, int start, int end, int y1,
277           boolean colourOnly)
278   {
279     /*
280      * if columns are all gapped, or sequence has no features, nothing to do
281      */
282     ContiguousI visiblePositions = seq.findPositions(start + 1, end + 1);
283     if (visiblePositions == null || !seq.getFeatures().hasFeatures()
284             && !av.isShowComplementFeatures())
285     {
286       return null;
287     }
288
289     updateFeatures();
290
291     if (transparency != 1f && g != null)
292     {
293       Graphics2D g2 = (Graphics2D) g;
294       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
295               transparency));
296     }
297
298     Color drawnColour = null;
299
300     /*
301      * draw 'complement' features below ours if configured to do so
302      */
303     if (av.isShowComplementFeatures()
304             && !av.isShowComplementFeaturesOnTop())
305     {
306       drawnColour = drawComplementFeatures(g, seq, start, end, y1,
307               colourOnly, visiblePositions, drawnColour);
308     }
309
310     /*
311      * iterate over features in ordering of their rendering (last is on top)
312      */
313     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
314     {
315       String type = renderOrder[renderIndex];
316       if (!showFeatureOfType(type))
317       {
318         continue;
319       }
320
321       FeatureColourI fc = getFeatureStyle(type);
322       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
323               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
324
325       if (fc.isSimpleColour())
326       {
327         filterFeaturesForDisplay(overlaps);
328       }
329
330       for (SequenceFeature sf : overlaps)
331       {
332         Color featureColour = getColor(sf, fc);
333         if (featureColour == null)
334         {
335           /*
336            * feature excluded by filters, or colour threshold
337            */
338           continue;
339         }
340
341         /*
342          * if feature starts/ends outside the visible range,
343          * restrict to visible positions (or if a contact feature,
344          * to a single position)
345          */
346         int visibleStart = sf.getBegin();
347         if (visibleStart < visiblePositions.getBegin())
348         {
349           visibleStart = sf.isContactFeature() ? sf.getEnd()
350                   : visiblePositions.getBegin();
351         }
352         int visibleEnd = sf.getEnd();
353         if (visibleEnd > visiblePositions.getEnd())
354         {
355           visibleEnd = sf.isContactFeature() ? sf.getBegin()
356                   : visiblePositions.getEnd();
357         }
358
359         int featureStartCol = seq.findIndex(visibleStart);
360         int featureEndCol = sf.begin == sf.end ? featureStartCol : seq
361                 .findIndex(visibleEnd);
362
363         // Color featureColour = getColour(sequenceFeature);
364
365         boolean isContactFeature = sf.isContactFeature();
366
367         if (isContactFeature)
368         {
369           boolean drawn = renderFeature(g, seq, featureStartCol - 1,
370                   featureStartCol - 1, featureColour, start, end, y1,
371                   colourOnly);
372           drawn |= renderFeature(g, seq, featureEndCol - 1,
373                   featureEndCol - 1, featureColour, start, end, y1,
374                   colourOnly);
375           if (drawn)
376           {
377             drawnColour = featureColour;
378           }
379         }
380         else
381         {
382           /*
383            * showing feature score by height of colour
384            * is not implemented as a selectable option 
385            *
386           if (av.isShowSequenceFeaturesHeight()
387                   && !Float.isNaN(sequenceFeature.score))
388           {
389             boolean drawn = renderScoreFeature(g, seq,
390                     seq.findIndex(sequenceFeature.begin) - 1,
391                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
392                     start, end, y1, normaliseScore(sequenceFeature),
393                     colourOnly);
394             if (drawn)
395             {
396               drawnColour = featureColour;
397             }
398           }
399           else
400           {
401           */
402             boolean drawn = renderFeature(g, seq,
403                     featureStartCol - 1,
404                     featureEndCol - 1, featureColour,
405                     start, end, y1, 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
466               .findComplementFeaturesAtResidue(seq, pos);
467       if (mf != null)
468       {
469         for (SequenceFeature sf : mf.features)
470         {
471           FeatureColourI fc = fr2.getFeatureStyle(sf.getType());
472           Color featureColour = fr2.getColor(sf, fc);
473           renderFeature(g, seq, column - 1, column - 1, featureColour,
474                   start, end, y1, colourOnly);
475           drawnColour = featureColour;
476         }
477       }
478     }
479     return drawnColour;
480   }
481
482   /**
483    * Called when alignment in associated view has new/modified features to
484    * discover and display.
485    * 
486    */
487   @Override
488   public void featuresAdded()
489   {
490     findAllFeatures();
491   }
492
493   /**
494    * Returns the sequence feature colour rendered at the given column position,
495    * or null if none found. The feature of highest render order (i.e. on top) is
496    * found, subject to both feature type and feature group being visible, and
497    * its colour returned. This method is suitable when no feature transparency
498    * applied (only the topmost visible feature colour is rendered).
499    * <p>
500    * Note this method does not check for a gap in the column so would return the
501    * colour for features enclosing a gapped column. Check for gap before calling
502    * if different behaviour is wanted.
503    * 
504    * @param seq
505    * @param column
506    *          (1..)
507    * @return
508    */
509   Color findFeatureColour(SequenceI seq, int column)
510   {
511     /*
512      * check for new feature added while processing
513      */
514     updateFeatures();
515
516     /*
517      * show complement features on top (if configured to show them)
518      */
519     if (av.isShowComplementFeatures() && av.isShowComplementFeaturesOnTop())
520     {
521       Color col = findComplementFeatureColour(seq, column);
522       if (col != null)
523       {
524         return col;
525       }
526     }
527
528     /*
529      * inspect features in reverse renderOrder (the last in the array is 
530      * displayed on top) until we find one that is rendered at the position
531      */
532     for (int renderIndex = renderOrder.length
533             - 1; renderIndex >= 0; renderIndex--)
534     {
535       String type = renderOrder[renderIndex];
536       if (!showFeatureOfType(type))
537       {
538         continue;
539       }
540
541       List<SequenceFeature> overlaps = seq.findFeatures(column, column,
542               type);
543       for (SequenceFeature sequenceFeature : overlaps)
544       {
545         if (!featureGroupNotShown(sequenceFeature))
546         {
547           Color col = getColour(sequenceFeature);
548           if (col != null)
549           {
550             return col;
551           }
552         }
553       }
554     }
555
556     /*
557      * show complement features underneath (if configured to show them)
558      */
559     Color col = null;
560     if (av.isShowComplementFeatures()
561             && !av.isShowComplementFeaturesOnTop())
562     {
563       col = findComplementFeatureColour(seq, column);
564     }
565
566     return col;
567   }
568
569   Color findComplementFeatureColour(SequenceI seq, int column)
570   {
571     AlignViewportI complement = av.getCodingComplement();
572     AlignFrame af = Desktop.getAlignFrameFor(complement);
573     FeatureRendererModel fr2 = af.getFeatureRenderer();
574     MappedFeatures mf = fr2.findComplementFeaturesAtResidue(
575             seq, seq.findPosition(column - 1));
576     if (mf == null)
577     {
578       return null;
579     }
580     ReverseListIterator<SequenceFeature> it = new ReverseListIterator<>(
581             mf.features);
582     while (it.hasNext())
583     {
584       SequenceFeature sf = it.next();
585       if (!fr2.featureGroupNotShown(sf))
586       {
587         Color col = fr2.getColour(sf);
588         if (col != null)
589         {
590           return col;
591         }
592       }
593     }
594     return null;
595   }
596 }