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