JAL-2526 correct treatment of contact features only partly visible
[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.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.List;
37
38 public class FeatureRenderer extends FeatureRendererModel
39 {
40   private static final AlphaComposite NO_TRANSPARENCY = AlphaComposite
41           .getInstance(AlphaComposite.SRC_OVER, 1.0f);
42
43   /**
44    * Constructor given a viewport
45    * 
46    * @param viewport
47    */
48   public FeatureRenderer(AlignViewportI viewport)
49   {
50     this.av = viewport;
51   }
52
53   /**
54    * Renders the sequence using the given feature colour between the given start
55    * and end columns. Returns true if at least one column is drawn, else false
56    * (the feature range does not overlap the start and end positions).
57    * 
58    * @param g
59    * @param seq
60    * @param featureStart
61    * @param featureEnd
62    * @param featureColour
63    * @param start
64    * @param end
65    * @param y1
66    * @param colourOnly
67    * @return
68    */
69   boolean renderFeature(Graphics g, SequenceI seq, int featureStart,
70           int featureEnd, Color featureColour, int start, int end, int y1,
71           boolean colourOnly)
72   {
73     int charHeight = av.getCharHeight();
74     int charWidth = av.getCharWidth();
75     boolean validCharWidth = av.isValidCharWidth();
76
77     if (featureStart > end || featureEnd < start)
78     {
79       return false;
80     }
81
82     if (featureStart < start)
83     {
84       featureStart = start;
85     }
86     if (featureEnd >= end)
87     {
88       featureEnd = end;
89     }
90     int pady = (y1 + charHeight) - charHeight / 5;
91
92     FontMetrics fm = g.getFontMetrics();
93     for (int i = featureStart; i <= featureEnd; i++)
94     {
95       char s = seq.getCharAt(i);
96
97       if (Comparison.isGap(s))
98       {
99         continue;
100       }
101
102       g.setColor(featureColour);
103
104       g.fillRect((i - start) * charWidth, y1, charWidth,
105               charHeight);
106
107       if (colourOnly || !validCharWidth)
108       {
109         continue;
110       }
111
112       g.setColor(Color.white);
113       int charOffset = (charWidth - fm.charWidth(s)) / 2;
114       g.drawString(String.valueOf(s), charOffset
115               + (charWidth * (i - start)), pady);
116     }
117     return true;
118   }
119
120   /**
121    * Renders the sequence using the given SCORE feature colour between the given
122    * start and end columns. Returns true if at least one column is drawn, else
123    * false (the feature range does not overlap the start and end positions).
124    * 
125    * @param g
126    * @param seq
127    * @param fstart
128    * @param fend
129    * @param featureColour
130    * @param start
131    * @param end
132    * @param y1
133    * @param bs
134    * @param colourOnly
135    * @return
136    */
137   boolean renderScoreFeature(Graphics g, SequenceI seq, int fstart,
138           int fend, Color featureColour, int start, int end, int y1,
139           byte[] bs, boolean colourOnly)
140   {
141     if (fstart > end || fend < start)
142     {
143       return false;
144     }
145
146     if (fstart < start)
147     { // fix for if the feature we have starts before the sequence start,
148       fstart = start; // but the feature end is still valid!!
149     }
150
151     if (fend >= end)
152     {
153       fend = end;
154     }
155     int charHeight = av.getCharHeight();
156     int pady = (y1 + charHeight) - charHeight / 5;
157     int ystrt = 0, yend = charHeight;
158     if (bs[0] != 0)
159     {
160       // signed - zero is always middle of residue line.
161       if (bs[1] < 128)
162       {
163         yend = charHeight * (128 - bs[1]) / 512;
164         ystrt = charHeight - yend / 2;
165       }
166       else
167       {
168         ystrt = charHeight / 2;
169         yend = charHeight * (bs[1] - 128) / 512;
170       }
171     }
172     else
173     {
174       yend = charHeight * bs[1] / 255;
175       ystrt = charHeight - yend;
176
177     }
178
179     FontMetrics fm = g.getFontMetrics();
180     int charWidth = av.getCharWidth();
181
182     for (int i = fstart; i <= fend; i++)
183     {
184       char s = seq.getCharAt(i);
185
186       if (Comparison.isGap(s))
187       {
188         continue;
189       }
190
191       g.setColor(featureColour);
192       int x = (i - start) * charWidth;
193       g.drawRect(x, y1, charWidth, charHeight);
194       g.fillRect(x, y1 + ystrt, charWidth, yend);
195
196       if (colourOnly || !av.isValidCharWidth())
197       {
198         continue;
199       }
200
201       g.setColor(Color.black);
202       int charOffset = (charWidth - fm.charWidth(s)) / 2;
203       g.drawString(String.valueOf(s), charOffset
204               + (charWidth * (i - start)), pady);
205     }
206     return true;
207   }
208
209   /**
210    * {@inheritDoc}
211    */
212   @Override
213   public Color findFeatureColour(SequenceI seq, int column, Graphics g)
214   {
215     if (!av.isShowSequenceFeatures())
216     {
217       return null;
218     }
219
220     if (Comparison.isGap(seq.getCharAt(column)))
221     {
222       /*
223        * returning null allows the colour scheme to provide gap colour
224        * - normally white, but can be customised
225        */
226       return null;
227     }
228
229     Color renderedColour = null;
230     if (transparency == 1.0f)
231     {
232       /*
233        * simple case - just find the topmost rendered visible feature colour
234        */
235       renderedColour = findFeatureColour(seq, column);
236     }
237     else
238     {
239       /*
240        * transparency case - draw all visible features in render order to
241        * build up a composite colour on the graphics context
242        */
243       renderedColour = drawSequence(g, seq, column, column, 0, true);
244     }
245     return renderedColour;
246   }
247
248   /**
249    * Draws the sequence features on the graphics context, or just determines the
250    * colour that would be drawn (if flag colourOnly is true). Returns the last
251    * colour drawn (which may not be the effective colour if transparency
252    * applies), or null if no feature is drawn in the range given.
253    * 
254    * @param g
255    *          the graphics context to draw on (may be null if colourOnly==true)
256    * @param seq
257    * @param start
258    *          start column
259    * @param end
260    *          end column
261    * @param y1
262    *          vertical offset at which to draw on the graphics
263    * @param colourOnly
264    *          if true, only do enough to determine the colour for the position,
265    *          do not draw the character
266    * @return
267    */
268   public synchronized Color drawSequence(final Graphics g,
269           final SequenceI seq, int start, int end, int y1,
270           boolean colourOnly)
271   {
272     /*
273      * if columns are all gapped, or sequence has no features, nothing to do
274      */
275     Range visiblePositions = seq.findPositions(start+1, end+1);
276     if (visiblePositions == null || !seq.getFeatures().hasFeatures())
277     {
278       return null;
279     }
280
281     updateFeatures();
282
283     if (transparency != 1f && g != null)
284     {
285       Graphics2D g2 = (Graphics2D) g;
286       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
287               transparency));
288     }
289
290     Color drawnColour = null;
291
292     /*
293      * iterate over features in ordering of their rendering (last is on top)
294      */
295     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
296     {
297       String type = renderOrder[renderIndex];
298       if (!showFeatureOfType(type))
299       {
300         continue;
301       }
302
303       FeatureColourI fc = getFeatureStyle(type);
304       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
305               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
306
307       filterFeaturesForDisplay(overlaps, fc);
308
309       for (SequenceFeature sf : overlaps)
310       {
311         Color featureColour = fc.getColor(sf);
312
313         /*
314          * if feature starts/ends outside the visible range,
315          * restrict to visible positions (or if a contact feature,
316          * to a single position)
317          */
318         int visibleStart = sf.getBegin();
319         if (visibleStart < visiblePositions.getBegin())
320         {
321           visibleStart = sf.isContactFeature() ? sf.getEnd()
322                   : visiblePositions.getBegin();
323         }
324         int visibleEnd = sf.getEnd();
325         if (visibleEnd > visiblePositions.getEnd())
326         {
327           visibleEnd = sf.isContactFeature() ? sf.getBegin()
328                   : visiblePositions.getEnd();
329         }
330
331         int featureStartCol = seq.findIndex(visibleStart);
332         int featureEndCol = sf.begin == sf.end ? featureStartCol : seq
333                 .findIndex(visibleEnd);
334
335         if (sf.isContactFeature())
336         {
337           boolean drawn = renderFeature(g, seq, featureStartCol - 1,
338                   featureStartCol - 1, featureColour, start, end, y1,
339                   colourOnly);
340           drawn |= renderFeature(g, seq, featureEndCol - 1,
341                   featureEndCol - 1, featureColour, start, end, y1,
342                   colourOnly);
343           if (drawn)
344           {
345             drawnColour = featureColour;
346           }
347         }
348         else if (showFeature(sf))
349         {
350           /*
351            * showing feature score by height of colour
352            * is not implemented as a selectable option 
353            *
354           if (av.isShowSequenceFeaturesHeight()
355                   && !Float.isNaN(sequenceFeature.score))
356           {
357             boolean drawn = renderScoreFeature(g, seq,
358                     seq.findIndex(sequenceFeature.begin) - 1,
359                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
360                     start, end, y1, normaliseScore(sequenceFeature),
361                     colourOnly);
362             if (drawn)
363             {
364               drawnColour = featureColour;
365             }
366           }
367           else
368           {
369           */
370             boolean drawn = renderFeature(g, seq,
371                     featureStartCol - 1,
372                     featureEndCol - 1, featureColour,
373                     start, end, y1, colourOnly);
374             if (drawn)
375             {
376               drawnColour = featureColour;
377             }
378           /*}*/
379         }
380       }
381     }
382
383     if (transparency != 1.0f && g != null)
384     {
385       /*
386        * reset transparency
387        */
388       Graphics2D g2 = (Graphics2D) g;
389       g2.setComposite(NO_TRANSPARENCY);
390     }
391
392     return drawnColour;
393   }
394
395   /**
396    * Called when alignment in associated view has new/modified features to
397    * discover and display.
398    * 
399    */
400   @Override
401   public void featuresAdded()
402   {
403     findAllFeatures();
404   }
405
406   /**
407    * Returns the sequence feature colour rendered at the given column position,
408    * or null if none found. The feature of highest render order (i.e. on top) is
409    * found, subject to both feature type and feature group being visible, and
410    * its colour returned.
411    * <p>
412    * Note this method does not check for a gap in the column so would return the
413    * colour for features enclosing a gapped column. Check for gap before calling
414    * if different behaviour is wanted.
415    * 
416    * @param seq
417    * @param column
418    *          (1..)
419    * @return
420    */
421   Color findFeatureColour(SequenceI seq, int column)
422   {
423     /*
424      * check for new feature added while processing
425      */
426     updateFeatures();
427
428     /*
429      * inspect features in reverse renderOrder (the last in the array is 
430      * displayed on top) until we find one that is rendered at the position
431      */
432     for (int renderIndex = renderOrder.length - 1; renderIndex >= 0; renderIndex--)
433     {
434       String type = renderOrder[renderIndex];
435       if (!showFeatureOfType(type))
436       {
437         continue;
438       }
439
440       List<SequenceFeature> overlaps = seq.findFeatures(column, column,
441               type);
442       for (SequenceFeature sequenceFeature : overlaps)
443       {
444         if (!featureGroupNotShown(sequenceFeature))
445         {
446           return getColour(sequenceFeature);
447         }
448       }
449     }
450   
451     /*
452      * no displayed feature found at position
453      */
454     return null;
455   }
456 }