JAL-3148 further bug fix to state of 'colourBy'
[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 = fr.getViewport().isShowSequenceFeatures() ? 
83             new FeatureColourFinder(fr) : null;
84     resColFinder = colFinder;
85
86     al = alignment;
87     shader = resshader;
88
89     pixelsPerCol = od.getPixelsPerCol();
90     pixelsPerSeq = od.getPixelsPerSeq();
91     graphHeight = od.getGraphHeight();
92     miniMe = new BufferedImage(od.getWidth(), od.getHeight(),
93             BufferedImage.TYPE_INT_RGB);
94   }
95
96   /**
97    * Draw alignment rows and columns onto an image
98    * 
99    * @param rit
100    *          Iterator over rows to be drawn
101    * @param cit
102    *          Iterator over columns to be drawn
103    * @return image containing the drawing
104    */
105   public BufferedImage draw(AlignmentRowsCollectionI rows,
106           AlignmentColsCollectionI cols)
107   {
108     int rgbcolor = Color.white.getRGB();
109     int seqIndex = 0;
110     int pixelRow = 0;
111     int alignmentHeight = miniMe.getHeight() - graphHeight;
112     int totalPixels = miniMe.getWidth() * alignmentHeight;
113
114     int lastRowUpdate = 0;
115     int lastUpdate = 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);
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           // store last update value
166           lastUpdate = sendProgressUpdate(
167                   (pixelCol + 1) * (endRow - pixelRow), totalPixels,
168                   lastRowUpdate, lastUpdate);
169
170           pixelCol = endCol + 1;
171         }
172         colIndex++;
173       }
174
175       if (pixelRow != endRow + 1)
176       {
177         // store row offset and last update value
178         lastRowUpdate = sendProgressUpdate(endRow + 1, alignmentHeight, 0,
179                 lastUpdate);
180         lastUpdate = lastRowUpdate;
181         pixelRow = endRow + 1;
182       }
183       seqIndex++;
184     }
185
186     overlayHiddenRegions(rows, cols);
187     // final update to progress bar if present
188     if (redraw)
189     {
190       sendProgressUpdate(pixelRow - 1, alignmentHeight, 0, 0);
191     }
192     else
193     {
194       sendProgressUpdate(alignmentHeight, miniMe.getHeight(), 0, 0);
195     }
196     return miniMe;
197   }
198
199   /*
200    * Calculate progress update value and fire event
201    * @param rowOffset number of rows to offset calculation by
202    * @return new rowOffset - return value only to be used when at end of a row
203    */
204   private int sendProgressUpdate(int position, int maximum, int rowOffset,
205           int lastUpdate)
206   {
207     int newUpdate = rowOffset
208             + Math.round(MAX_PROGRESS * ((float) position / maximum));
209     if (newUpdate > lastUpdate)
210     {
211       changeSupport.firePropertyChange(UPDATE, rowOffset, newUpdate);
212       return newUpdate;
213     }
214     return newUpdate;
215   }
216
217   /*
218    * Find the RGB value of the colour of a sequence at a specified column position
219    * 
220    * @param seq
221    *          sequence to get colour for
222    * @param lastcol
223    *          column position to get colour for
224    * @return colour of sequence at this position, as RGB
225    */
226   int getColumnColourFromSequence(SequenceGroup[] allGroups,
227           SequenceI seq, int lastcol)
228   {
229     Color color = resColFinder.GAP_COLOUR;
230
231     if ((seq != null) && (seq.getLength() > lastcol))
232     {
233       color = resColFinder.getResidueColour(true, shader, allGroups, seq,
234               lastcol, finder);
235     }
236
237     return color.getRGB();
238   }
239
240   /**
241    * Overlay the hidden regions on the overview image
242    * 
243    * @param rows
244    *          collection of rows the overview is built over
245    * @param cols
246    *          collection of columns the overview is built over
247    */
248   private void overlayHiddenRegions(AlignmentRowsCollectionI rows,
249           AlignmentColsCollectionI cols)
250   {
251     if (cols.hasHidden() || rows.hasHidden())
252     {
253       BufferedImage mask = buildHiddenImage(rows, cols, miniMe.getWidth(),
254               miniMe.getHeight());
255
256       Graphics2D g = (Graphics2D) miniMe.getGraphics();
257       g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
258               TRANSPARENCY));
259       g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null);
260     }
261   }
262
263   /**
264    * Build a masking image of hidden columns and rows to be applied on top of
265    * the main overview image.
266    * 
267    * @param rows
268    *          collection of rows the overview is built over
269    * @param cols
270    *          collection of columns the overview is built over
271    * @param width
272    *          width of overview in pixels
273    * @param height
274    *          height of overview in pixels
275    * @return BufferedImage containing mask of hidden regions
276    */
277   private BufferedImage buildHiddenImage(AlignmentRowsCollectionI rows,
278           AlignmentColsCollectionI cols, int width, int height)
279   {
280     // new masking image
281     BufferedImage hiddenImage = new BufferedImage(width, height,
282             BufferedImage.TYPE_INT_ARGB);
283
284     int colIndex = 0;
285     int pixelCol = 0;
286
287     Color hidden = resColFinder.getHiddenColour();
288
289     Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics();
290
291     // set background to transparent
292     g2d.setComposite(AlphaComposite.Clear);
293     g2d.fillRect(0, 0, width, height);
294
295     // set next colour to opaque
296     g2d.setComposite(AlphaComposite.Src);
297
298     for (int alignmentCol : cols)
299     {
300       if (redraw)
301       {
302         break;
303       }
304
305       // calculate where this column extends to in pixels
306       int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
307               hiddenImage.getWidth() - 1);
308
309       if (pixelCol <= endCol)
310       {
311         // determine the colour based on the sequence and column position
312         if (cols.isHidden(alignmentCol))
313         {
314           g2d.setColor(hidden);
315           g2d.fillRect(pixelCol, 0, endCol - pixelCol + 1, height);
316         }
317
318         pixelCol = endCol + 1;
319       }
320       colIndex++;
321
322     }
323
324     int seqIndex = 0;
325     int pixelRow = 0;
326     for (int alignmentRow : rows)
327     {
328       if (redraw)
329       {
330         break;
331       }
332
333       // calculate where this row extends to in pixels
334       int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
335               miniMe.getHeight() - 1);
336
337       // get details of this alignment row
338       if (rows.isHidden(alignmentRow))
339       {
340         g2d.setColor(hidden);
341         g2d.fillRect(0, pixelRow, width, endRow - pixelRow + 1);
342       }
343       pixelRow = endRow + 1;
344       seqIndex++;
345     }
346
347     return hiddenImage;
348   }
349
350   /**
351    * Draw the alignment annotation in the overview panel
352    * 
353    * @param g
354    *          the graphics object to draw on
355    * @param anno
356    *          alignment annotation information
357    * @param y
358    *          y-position for the annotation graph
359    * @param cols
360    *          the collection of columns used in the overview panel
361    */
362   public void drawGraph(Graphics g, AlignmentAnnotation anno, int y,
363           AlignmentColsCollectionI cols)
364   {
365     Annotation[] annotations = anno.annotations;
366     g.setColor(Color.white);
367     g.fillRect(0, 0, miniMe.getWidth(), y);
368
369     int height;
370     int colIndex = 0;
371     int pixelCol = 0;
372     for (int alignmentCol : cols)
373     {
374       if (redraw)
375       {
376         changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
377         break;
378       }
379
380       if (alignmentCol >= annotations.length)
381       {
382         break; // no more annotations to draw here
383       }
384       else
385       {
386         int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
387                 miniMe.getWidth() - 1);
388
389         if (annotations[alignmentCol] != null)
390         {
391           if (annotations[alignmentCol].colour == null)
392           {
393             g.setColor(Color.black);
394           }
395           else
396           {
397             g.setColor(annotations[alignmentCol].colour);
398           }
399
400           height = (int) ((annotations[alignmentCol].value / anno.graphMax)
401                   * y);
402           if (height > y)
403           {
404             height = y;
405           }
406
407           g.fillRect(pixelCol, y - height, endCol - pixelCol + 1, height);
408         }
409
410         pixelCol = endCol + 1;
411         colIndex++;
412       }
413     }
414     changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
415             MAX_PROGRESS);
416   }
417
418   /**
419    * Allows redraw flag to be set
420    * 
421    * @param b
422    *          value to set redraw to: true = redraw is occurring, false = no
423    *          redraw
424    */
425   public void setRedraw(boolean b)
426   {
427     synchronized (this)
428     {
429       redraw = b;
430     }
431   }
432
433   public void addPropertyChangeListener(RendererListenerI listener)
434   {
435     changeSupport.addPropertyChangeListener(listener);
436   }
437
438   public void removePropertyChangeListener(RendererListenerI listener)
439   {
440     changeSupport.removePropertyChangeListener(listener);
441   }
442 }