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