de3b201e83e13bb14b0c7e06527780dc6ab33f5a
[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.datamodel.SequenceFeature;
25 import jalview.datamodel.SequenceI;
26 import jalview.util.Comparison;
27 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
28
29 import java.awt.AlphaComposite;
30 import java.awt.Color;
31 import java.awt.FontMetrics;
32 import java.awt.Graphics;
33 import java.awt.Graphics2D;
34 import java.awt.image.BufferedImage;
35
36 public class FeatureRenderer extends FeatureRendererModel
37 {
38   private static final AlphaComposite NO_TRANSPARENCY = AlphaComposite
39           .getInstance(AlphaComposite.SRC_OVER, 1.0f);
40
41   protected SequenceI lastSeq;
42
43   BufferedImage offscreenImage;
44
45   private volatile SequenceFeature[] lastSequenceFeatures;
46
47   int sfSize;
48
49   /**
50    * Constructor given a viewport
51    * 
52    * @param viewport
53    */
54   public FeatureRenderer(AlignViewportI viewport)
55   {
56     this.av = viewport;
57   }
58
59   /**
60    * Renders the sequence using the given feature colour between the given start
61    * and end columns. Returns true if at least one column is drawn, else false
62    * (the feature range does not overlap the start and end positions).
63    * 
64    * @param g
65    * @param seq
66    * @param featureStart
67    * @param featureEnd
68    * @param featureColour
69    * @param start
70    * @param end
71    * @param y1
72    * @param colourOnly
73    * @return
74    */
75   boolean renderFeature(Graphics g, SequenceI seq, int featureStart,
76           int featureEnd, Color featureColour, int start, int end, int y1,
77           boolean colourOnly)
78   {
79     int charHeight = av.getCharHeight();
80     int charWidth = av.getCharWidth();
81     boolean validCharWidth = av.isValidCharWidth();
82
83     if (featureStart > end || featureEnd < start)
84     {
85       return false;
86     }
87
88     if (featureStart < start)
89     {
90       featureStart = start;
91     }
92     if (featureEnd >= end)
93     {
94       featureEnd = end;
95     }
96     int pady = (y1 + charHeight) - charHeight / 5;
97
98     FontMetrics fm = g.getFontMetrics();
99     for (int i = featureStart; i <= featureEnd; i++)
100     {
101       char s = seq.getCharAt(i);
102
103       if (Comparison.isGap(s))
104       {
105         continue;
106       }
107
108       g.setColor(featureColour);
109
110       g.fillRect((i - start) * charWidth, y1, charWidth,
111               charHeight);
112
113       if (colourOnly || !validCharWidth)
114       {
115         continue;
116       }
117
118       g.setColor(Color.white);
119       int charOffset = (charWidth - fm.charWidth(s)) / 2;
120       g.drawString(String.valueOf(s), charOffset
121               + (charWidth * (i - start)), pady);
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), charOffset
210               + (charWidth * (i - start)), pady);
211     }
212     return true;
213   }
214
215   /**
216    * This is used by Structure Viewers and the Overview Window to get the
217    * feature colour of the rendered sequence
218    * 
219    * @param defaultColour
220    * @param seq
221    * @param column
222    * @return
223    */
224   @Override
225   public Color findFeatureColour(Color defaultColour, SequenceI seq,
226           int column, Graphics g)
227   {
228     if (!av.isShowSequenceFeatures())
229     {
230       return defaultColour;
231     }
232
233     SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
234     if (seq != lastSeq)
235     {
236       lastSeq = seq;
237       lastSequenceFeatures = sequenceFeatures;
238       if (lastSequenceFeatures != null)
239       {
240         sfSize = lastSequenceFeatures.length;
241       }
242     }
243     else
244     {
245       if (lastSequenceFeatures != sequenceFeatures)
246       {
247         lastSequenceFeatures = sequenceFeatures;
248         if (lastSequenceFeatures != null)
249         {
250           sfSize = lastSequenceFeatures.length;
251         }
252       }
253     }
254
255     if (lastSequenceFeatures == null || sfSize == 0)
256     {
257       return defaultColour;
258     }
259
260     if (Comparison.isGap(lastSeq.getCharAt(column)))
261     {
262       return Color.white;
263     }
264
265     Color renderedColour = null;
266     if (transparency == 1.0f)
267     {
268       renderedColour = findFeatureColour(seq, seq.findPosition(column));
269     }
270     else
271     {
272       renderedColour = drawSequence(g, lastSeq, column, column, 0, true);
273     }
274     return renderedColour == null ? defaultColour : renderedColour;
275   }
276
277   /**
278    * Draws the sequence features on the graphics context, or just determines the
279    * colour that would be drawn (if flag offscreenrender is true).
280    * 
281    * @param g
282    *          the graphics context to draw on (may be null if colourOnly==true)
283    * @param seq
284    * @param start
285    *          start column (or sequence position in offscreenrender mode)
286    * @param end
287    *          end column (not used in offscreenrender mode)
288    * @param y1
289    *          vertical offset at which to draw on the graphics
290    * @param colourOnly
291    *          if true, only do enough to determine the colour for the position,
292    *          do not draw the character
293    * @return
294    */
295   public synchronized Color drawSequence(final Graphics g,
296           final SequenceI seq, int start, int end, int y1,
297           boolean colourOnly)
298   {
299     SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
300     if (sequenceFeatures == null || sequenceFeatures.length == 0)
301     {
302       return null;
303     }
304
305     updateFeatures();
306
307     if (lastSeq == null || seq != lastSeq
308             || sequenceFeatures != lastSequenceFeatures)
309     {
310       lastSeq = seq;
311       lastSequenceFeatures = sequenceFeatures;
312     }
313
314     if (transparency != 1f && g != null)
315     {
316       Graphics2D g2 = (Graphics2D) g;
317       g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
318               transparency));
319     }
320
321     int startPos = lastSeq.findPosition(start);
322     int endPos = lastSeq.findPosition(end);
323
324     sfSize = lastSequenceFeatures.length;
325     Color drawnColour = null;
326
327     /*
328      * iterate over features in ordering of their rendering;
329      * if drawing a range of columns, use render order to ensure last is on top
330      * if drawing a single column (as in findFeatureColour), with no 
331      * transparency, work backwards to find the topmost rendered feature colour
332      */
333     for (int renderIndex = 0; renderIndex < renderOrder.length; renderIndex++)
334     {
335       String type = renderOrder[renderIndex];
336       if (!showFeatureOfType(type))
337       {
338         continue;
339       }
340
341       // loop through all features in sequence to find
342       // current feature to render
343       for (int sfindex = 0; sfindex < sfSize; sfindex++)
344       {
345         final SequenceFeature sequenceFeature = lastSequenceFeatures[sfindex];
346         if (!sequenceFeature.type.equals(type))
347         {
348           continue;
349         }
350
351         if (featureGroupNotShown(sequenceFeature))
352         {
353           continue;
354         }
355
356         /*
357          * check feature overlaps the visible part of the alignment, 
358          * unless doing offscreenRender (to the Overview window or a 
359          * structure viewer) which is not limited 
360          */
361         if (sequenceFeature.getBegin() > endPos
362                 || sequenceFeature.getEnd() < startPos)
363         {
364           continue;
365         }
366
367         Color featureColour = getColour(sequenceFeature);
368         boolean isContactFeature = sequenceFeature.isContactFeature();
369
370         if (isContactFeature)
371         {
372           boolean drawn = renderFeature(g, seq,
373                   seq.findIndex(sequenceFeature.begin) - 1,
374                   seq.findIndex(sequenceFeature.begin) - 1, featureColour,
375                   start, end, y1, colourOnly);
376           drawn |= renderFeature(g, seq,
377                   seq.findIndex(sequenceFeature.end) - 1,
378                   seq.findIndex(sequenceFeature.end) - 1, featureColour,
379                   start, end, y1, colourOnly);
380           if (drawn)
381           {
382             drawnColour = featureColour;
383           }
384         }
385         else if (showFeature(sequenceFeature))
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             boolean drawn = renderFeature(g, seq,
403                     seq.findIndex(sequenceFeature.begin) - 1,
404                     seq.findIndex(sequenceFeature.end) - 1, featureColour,
405                     start, end, y1, colourOnly);
406             if (drawn)
407             {
408               drawnColour = featureColour;
409             }
410           }
411         }
412       }
413     }
414
415     if (transparency != 1.0f && g != null)
416     {
417       /*
418        * get colour as rendered including transparency
419        * and reset transparency
420        */
421       if (offscreenImage != null && drawnColour != null)
422       {
423         drawnColour = new Color(offscreenImage.getRGB(0, 0));
424       }
425       Graphics2D g2 = (Graphics2D) g;
426       g2.setComposite(NO_TRANSPARENCY);
427     }
428
429     return drawnColour;
430   }
431
432   /**
433    * Answers true if the feature belongs to a feature group which is not
434    * currently displayed, else false
435    * 
436    * @param sequenceFeature
437    * @return
438    */
439   protected boolean featureGroupNotShown(
440           final SequenceFeature sequenceFeature)
441   {
442     return featureGroups != null
443             && sequenceFeature.featureGroup != null
444             && sequenceFeature.featureGroup.length() != 0
445             && featureGroups.containsKey(sequenceFeature.featureGroup)
446             && !featureGroups.get(sequenceFeature.featureGroup)
447                     .booleanValue();
448   }
449
450   /**
451    * Called when alignment in associated view has new/modified features to
452    * discover and display.
453    * 
454    */
455   @Override
456   public void featuresAdded()
457   {
458     lastSeq = null;
459     findAllFeatures();
460   }
461
462   /**
463    * Returns the sequence feature colour rendered at the given sequence
464    * position, or null if none found. The feature of highest render order (i.e.
465    * on top) is found, subject to both feature type and feature group being
466    * visible, and its colour returned.
467    * 
468    * @param seq
469    * @param pos
470    * @return
471    */
472   Color findFeatureColour(SequenceI seq, int pos)
473   {
474     SequenceFeature[] sequenceFeatures = seq.getSequenceFeatures();
475     if (sequenceFeatures == null || sequenceFeatures.length == 0)
476     {
477       return null;
478     }
479   
480     // updateFeatures();
481
482     /*
483      * inspect features in reverse renderOrder (the last in the array is 
484      * displayed on top) until we find one that is rendered at the position
485      */
486     for (int renderIndex = renderOrder.length - 1; renderIndex >= 0; renderIndex--)
487     {
488       String type = renderOrder[renderIndex];
489       if (!showFeatureOfType(type))
490       {
491         continue;
492       }
493
494       for (int sfindex = 0; sfindex < sequenceFeatures.length; sfindex++)
495       {
496         SequenceFeature sequenceFeature = sequenceFeatures[sfindex];
497         if (!sequenceFeature.type.equals(type))
498         {
499           continue;
500         }
501
502         if (featureGroupNotShown(sequenceFeature))
503         {
504           continue;
505         }
506
507         boolean featureIsAtPosition = sequenceFeature.begin <= pos
508                 && sequenceFeature.end >= pos;
509         if (sequenceFeature.isContactFeature())
510         {
511           featureIsAtPosition = sequenceFeature.begin == pos
512                   || sequenceFeature.end == pos;
513         }
514         if (featureIsAtPosition)
515         {
516           return getColour(sequenceFeature);
517         }
518       }
519     }
520   
521     /*
522      * no displayed feature found at position
523      */
524     return null;
525   }
526 }