JAL-3187 basic first version for tooltips only (rendering yet to do)
[jalview.git] / src / jalview / renderer / OverviewRenderer.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;
22
23 import jalview.api.AlignmentColsCollectionI;
24 import jalview.api.AlignmentRowsCollectionI;
25 import jalview.api.RendererListenerI;
26 import jalview.datamodel.AlignmentAnnotation;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.Annotation;
29 import jalview.datamodel.SequenceGroup;
30 import jalview.datamodel.SequenceI;
31 import jalview.renderer.seqfeatures.FeatureColourFinder;
32 import jalview.renderer.seqfeatures.FeatureRenderer;
33 import jalview.viewmodel.OverviewDimensions;
34
35 import java.awt.AlphaComposite;
36 import java.awt.Color;
37 import java.awt.Graphics;
38 import java.awt.Graphics2D;
39 import java.awt.image.BufferedImage;
40 import java.beans.PropertyChangeSupport;
41
42 public class OverviewRenderer
43 {
44   // transparency of hidden cols/seqs overlay
45   private final float TRANSPARENCY = 0.5f;
46
47   public static final String UPDATE = "OverviewUpdate";
48
49   private static final int MAX_PROGRESS = 100;
50
51   private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
52           this);
53
54   private FeatureColourFinder finder;
55
56   // image to render on
57   private BufferedImage miniMe;
58
59   // raw number of pixels to allocate to each column
60   private float pixelsPerCol;
61
62   // raw number of pixels to allocate to each row
63   private float pixelsPerSeq;
64
65   // height in pixels of graph
66   private int graphHeight;
67
68   // flag to indicate whether to halt drawing
69   private volatile boolean redraw = false;
70
71   // reference to alignment, needed to get sequence groups
72   private AlignmentI al;
73
74   private ResidueShaderI shader;
75
76   private OverviewResColourFinder resColFinder;
77
78   public OverviewRenderer(FeatureRenderer fr, OverviewDimensions od,
79           AlignmentI alignment,
80           ResidueShaderI resshader, OverviewResColourFinder colFinder)
81   {
82     finder = new FeatureColourFinder(fr);
83     resColFinder = colFinder;
84
85     al = alignment;
86     shader = resshader;
87
88     pixelsPerCol = od.getPixelsPerCol();
89     pixelsPerSeq = od.getPixelsPerSeq();
90     graphHeight = od.getGraphHeight();
91     miniMe = new BufferedImage(od.getWidth(), od.getHeight(),
92             BufferedImage.TYPE_INT_RGB);
93   }
94
95   /**
96    * Draw alignment rows and columns onto an image
97    * 
98    * @param rit
99    *          Iterator over rows to be drawn
100    * @param cit
101    *          Iterator over columns to be drawn
102    * @return image containing the drawing
103    */
104   public BufferedImage draw(AlignmentRowsCollectionI rows,
105           AlignmentColsCollectionI cols)
106   {
107     int rgbcolor = Color.white.getRGB();
108     int seqIndex = 0;
109     int pixelRow = 0;
110     int alignmentHeight = miniMe.getHeight() - graphHeight;
111     int totalPixels = miniMe.getWidth() * alignmentHeight;
112
113     int lastRowUpdate = 0;
114     int lastUpdate = 0;
115     changeSupport.firePropertyChange(UPDATE, -1, 0);
116
117     for (int alignmentRow : rows)
118     {
119       if (redraw)
120       {
121         break;
122       }
123     
124       // get details of this alignment row
125       SequenceI seq = rows.getSequence(alignmentRow);
126
127       // rate limiting step when rendering overview for lots of groups
128       SequenceGroup[] allGroups = al.findAllGroups(seq);
129
130       // calculate where this row extends to in pixels
131       int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
132               miniMe.getHeight() - 1);
133     
134       int colIndex = 0;
135       int pixelCol = 0;
136       for (int alignmentCol : cols)
137       {
138         if (redraw)
139         {
140           break;
141         }
142     
143         // calculate where this column extends to in pixels
144         int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
145                 miniMe.getWidth() - 1);
146     
147         // don't do expensive colour determination if we're not going to use it
148         // NB this is important to avoid performance issues in the overview
149         // panel
150         if (pixelCol <= endCol)
151         {
152           rgbcolor = getColumnColourFromSequence(allGroups, seq,
153                   alignmentCol);
154     
155           // fill in the appropriate number of pixels
156           for (int row = pixelRow; row <= endRow; ++row)
157           {
158             for (int col = pixelCol; col <= endCol; ++col)
159             {
160               miniMe.setRGB(col, row, rgbcolor);
161             }
162           }
163
164           // store last update value
165           lastUpdate = sendProgressUpdate(
166                   (pixelCol + 1) * (endRow - pixelRow), totalPixels,
167                   lastRowUpdate, lastUpdate);
168
169           pixelCol = endCol + 1;
170         }
171         colIndex++;
172       }
173
174       if (pixelRow != endRow + 1)
175       {
176         // store row offset and last update value
177         lastRowUpdate = sendProgressUpdate(endRow + 1, alignmentHeight, 0,
178                 lastUpdate);
179         lastUpdate = lastRowUpdate;
180         pixelRow = endRow + 1;
181       }
182       seqIndex++;
183     }
184
185     overlayHiddenRegions(rows, cols);
186     // final update to progress bar if present
187     if (redraw)
188     {
189       sendProgressUpdate(pixelRow - 1, alignmentHeight, 0, 0);
190     }
191     else
192     {
193       sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
194     }
195     return miniMe;
196   }
197
198   /*
199    * Calculate progress update value and fire event
200    * @param rowOffset number of rows to offset calculation by
201    * @return new rowOffset - return value only to be used when at end of a row
202    */
203   private int sendProgressUpdate(int position, int maximum, int rowOffset,
204           int lastUpdate)
205   {
206     int newUpdate = rowOffset
207             + Math.round(MAX_PROGRESS * ((float) position / maximum));
208     if (newUpdate > lastUpdate)
209     {
210       changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate);
211       return newUpdate;
212     }
213     return newUpdate;
214   }
215
216   /*
217    * Find the RGB value of the colour of a sequence at a specified column position
218    * 
219    * @param seq
220    *          sequence to get colour for
221    * @param lastcol
222    *          column position to get colour for
223    * @return colour of sequence at this position, as RGB
224    */
225   int getColumnColourFromSequence(SequenceGroup[] allGroups,
226           SequenceI seq, int lastcol)
227   {
228     Color color = resColFinder.GAP_COLOUR;
229
230     if ((seq != null) && (seq.getLength() > lastcol))
231     {
232       color = resColFinder.getResidueColour(true, shader, allGroups, seq,
233               lastcol, finder);
234     }
235
236     return color.getRGB();
237   }
238
239   /**
240    * Overlay the hidden regions on the overview image
241    * 
242    * @param rows
243    *          collection of rows the overview is built over
244    * @param cols
245    *          collection of columns the overview is built over
246    */
247   private void overlayHiddenRegions(AlignmentRowsCollectionI rows,
248           AlignmentColsCollectionI cols)
249   {
250     if (cols.hasHidden() || rows.hasHidden())
251     {
252       BufferedImage mask = buildHiddenImage(rows, cols, miniMe.getWidth(),
253               miniMe.getHeight());
254
255       Graphics2D g = (Graphics2D) miniMe.getGraphics();
256       g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
257               TRANSPARENCY));
258       g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null);
259     }
260   }
261
262   /**
263    * Build a masking image of hidden columns and rows to be applied on top of
264    * the main overview image.
265    * 
266    * @param rows
267    *          collection of rows the overview is built over
268    * @param cols
269    *          collection of columns the overview is built over
270    * @param width
271    *          width of overview in pixels
272    * @param height
273    *          height of overview in pixels
274    * @return BufferedImage containing mask of hidden regions
275    */
276   private BufferedImage buildHiddenImage(AlignmentRowsCollectionI rows,
277           AlignmentColsCollectionI cols, int width, int height)
278   {
279     // new masking image
280     BufferedImage hiddenImage = new BufferedImage(width, height,
281             BufferedImage.TYPE_INT_ARGB);
282
283     int colIndex = 0;
284     int pixelCol = 0;
285
286     Color hidden = resColFinder.getHiddenColour();
287
288     Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics();
289
290     // set background to transparent
291     g2d.setComposite(AlphaComposite.Clear);
292     g2d.fillRect(0, 0, width, height);
293
294     // set next colour to opaque
295     g2d.setComposite(AlphaComposite.Src);
296
297     for (int alignmentCol : cols)
298     {
299       if (redraw)
300       {
301         break;
302       }
303
304       // calculate where this column extends to in pixels
305       int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
306               hiddenImage.getWidth() - 1);
307
308       if (pixelCol <= endCol)
309       {
310         // determine the colour based on the sequence and column position
311         if (cols.isHidden(alignmentCol))
312         {
313           g2d.setColor(hidden);
314           g2d.fillRect(pixelCol, 0, endCol - pixelCol + 1, height);
315         }
316
317         pixelCol = endCol + 1;
318       }
319       colIndex++;
320
321     }
322
323     int seqIndex = 0;
324     int pixelRow = 0;
325     for (int alignmentRow : rows)
326     {
327       if (redraw)
328       {
329         break;
330       }
331
332       // calculate where this row extends to in pixels
333       int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
334               miniMe.getHeight() - 1);
335
336       // get details of this alignment row
337       if (rows.isHidden(alignmentRow))
338       {
339         g2d.setColor(hidden);
340         g2d.fillRect(0, pixelRow, width, endRow - pixelRow + 1);
341       }
342       pixelRow = endRow + 1;
343       seqIndex++;
344     }
345
346     return hiddenImage;
347   }
348
349   /**
350    * Draw the alignment annotation in the overview panel
351    * 
352    * @param g
353    *          the graphics object to draw on
354    * @param anno
355    *          alignment annotation information
356    * @param y
357    *          y-position for the annotation graph
358    * @param cols
359    *          the collection of columns used in the overview panel
360    */
361   public void drawGraph(Graphics g, AlignmentAnnotation anno, int y,
362           AlignmentColsCollectionI cols)
363   {
364     Annotation[] annotations = anno.annotations;
365     g.setColor(Color.white);
366     g.fillRect(0, 0, miniMe.getWidth(), y);
367
368     int height;
369     int colIndex = 0;
370     int pixelCol = 0;
371     for (int alignmentCol : cols)
372     {
373       if (redraw)
374       {
375         changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
376         break;
377       }
378
379       if (alignmentCol >= annotations.length)
380       {
381         break; // no more annotations to draw here
382       }
383       else
384       {
385         int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
386                 miniMe.getWidth() - 1);
387
388         if (annotations[alignmentCol] != null)
389         {
390           if (annotations[alignmentCol].colour == null)
391           {
392             g.setColor(Color.black);
393           }
394           else
395           {
396             g.setColor(annotations[alignmentCol].colour);
397           }
398
399           height = (int) ((annotations[alignmentCol].value / anno.graphMax)
400                   * y);
401           if (height > y)
402           {
403             height = y;
404           }
405
406           g.fillRect(pixelCol, y - height, endCol - pixelCol + 1, height);
407         }
408
409         pixelCol = endCol + 1;
410         colIndex++;
411       }
412     }
413     changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
414             MAX_PROGRESS);
415   }
416
417   /**
418    * Allows redraw flag to be set
419    * 
420    * @param b
421    *          value to set redraw to: true = redraw is occurring, false = no
422    *          redraw
423    */
424   public void setRedraw(boolean b)
425   {
426     synchronized (this)
427     {
428       redraw = b;
429     }
430   }
431
432   public void addPropertyChangeListener(RendererListenerI listener)
433   {
434     changeSupport.addPropertyChangeListener(listener);
435   }
436
437   public void removePropertyChangeListener(RendererListenerI listener)
438   {
439     changeSupport.removePropertyChangeListener(listener);
440   }
441 }