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