JAL-2754 reinstate PA import from bad conflict merge
[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 = resColFinder.GAP_COLOUR;
234
235     if ((seq != null) && (seq.getLength() > lastcol))
236     {
237       color = resColFinder.getResidueColour(true, shader, allGroups, seq,
238               lastcol, fcfinder);
239     }
240
241     return color.getRGB();
242   }
243
244   /**
245    * Overlay the hidden regions on the overview image
246    * 
247    * @param rows
248    *          collection of rows the overview is built over
249    * @param cols
250    *          collection of columns the overview is built over
251    */
252   private void overlayHiddenRegions(AlignmentRowsCollectionI rows,
253           AlignmentColsCollectionI cols)
254   {
255     if (cols.hasHidden() || rows.hasHidden())
256     {
257       BufferedImage mask = buildHiddenImage(rows, cols, miniMe.getWidth(),
258               miniMe.getHeight());
259
260       Graphics2D g = (Graphics2D) miniMe.getGraphics();
261       g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
262               TRANSPARENCY));
263       g.drawImage(mask, 0, 0, miniMe.getWidth(), miniMe.getHeight(), null);
264     }
265   }
266
267   /**
268    * Build a masking image of hidden columns and rows to be applied on top of
269    * the main overview image.
270    * 
271    * @param rows
272    *          collection of rows the overview is built over
273    * @param cols
274    *          collection of columns the overview is built over
275    * @param width
276    *          width of overview in pixels
277    * @param height
278    *          height of overview in pixels
279    * @return BufferedImage containing mask of hidden regions
280    */
281   private BufferedImage buildHiddenImage(AlignmentRowsCollectionI rows,
282           AlignmentColsCollectionI cols, int width, int height)
283   {
284     // new masking image
285     BufferedImage hiddenImage = new BufferedImage(width, height,
286             BufferedImage.TYPE_INT_ARGB);
287
288     int colIndex = 0;
289     int pixelCol = 0;
290
291     Color hidden = resColFinder.getHiddenColour();
292
293     Graphics2D g2d = (Graphics2D) hiddenImage.getGraphics();
294
295     // set background to transparent
296     g2d.setComposite(AlphaComposite.Clear);
297     g2d.fillRect(0, 0, width, height);
298
299     // set next colour to opaque
300     g2d.setComposite(AlphaComposite.Src);
301
302     for (int alignmentCol : cols)
303     {
304       if (redraw)
305       {
306         break;
307       }
308
309       // calculate where this column extends to in pixels
310       int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
311               hiddenImage.getWidth() - 1);
312
313       if (pixelCol <= endCol)
314       {
315         // determine the colour based on the sequence and column position
316         if (cols.isHidden(alignmentCol))
317         {
318           g2d.setColor(hidden);
319           g2d.fillRect(pixelCol, 0, endCol - pixelCol + 1, height);
320         }
321
322         pixelCol = endCol + 1;
323       }
324       colIndex++;
325
326     }
327
328     int seqIndex = 0;
329     int pixelRow = 0;
330     for (int alignmentRow : rows)
331     {
332       if (redraw)
333       {
334         break;
335       }
336
337       // calculate where this row extends to in pixels
338       int endRow = Math.min(Math.round((seqIndex + 1) * pixelsPerSeq) - 1,
339               miniMe.getHeight() - 1);
340
341       // get details of this alignment row
342       if (rows.isHidden(alignmentRow))
343       {
344         g2d.setColor(hidden);
345         g2d.fillRect(0, pixelRow, width, endRow - pixelRow + 1);
346       }
347       pixelRow = endRow + 1;
348       seqIndex++;
349     }
350
351     return hiddenImage;
352   }
353
354   /**
355    * Draw the alignment annotation in the overview panel
356    * 
357    * @param g
358    *          the graphics object to draw on
359    * @param anno
360    *          alignment annotation information
361    * @param charWidth
362    *          alignment character width value
363    * @param y
364    *          y-position for the annotation graph
365    * @param cols
366    *          the collection of columns used in the overview panel
367    */
368   public void drawGraph(Graphics g, AlignmentAnnotation anno, int charWidth,
369           int y, AlignmentColsCollectionI cols)
370   {
371     Annotation[] annotations = anno.annotations;
372     g.setColor(Color.white);
373     g.fillRect(0, 0, miniMe.getWidth(), y);
374
375     int height;
376     int colIndex = 0;
377     int pixelCol = 0;
378     for (int alignmentCol : cols)
379     {
380       if (redraw)
381       {
382         changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1, 0);
383         break;
384       }
385
386       if (alignmentCol >= annotations.length)
387       {
388         break; // no more annotations to draw here
389       }
390       else
391       {
392         int endCol = Math.min(Math.round((colIndex + 1) * pixelsPerCol) - 1,
393                 miniMe.getWidth() - 1);
394
395         if (annotations[alignmentCol] != null)
396         {
397           if (annotations[alignmentCol].colour == null)
398           {
399             g.setColor(Color.black);
400           }
401           else
402           {
403             g.setColor(annotations[alignmentCol].colour);
404           }
405
406           height = (int) ((annotations[alignmentCol].value / anno.graphMax)
407                   * y);
408           if (height > y)
409           {
410             height = y;
411           }
412
413           g.fillRect(pixelCol, y - height, endCol - pixelCol + 1, height);
414         }
415
416         pixelCol = endCol + 1;
417         colIndex++;
418       }
419     }
420     changeSupport.firePropertyChange(UPDATE, MAX_PROGRESS - 1,
421             MAX_PROGRESS);
422   }
423
424   /**
425    * Allows redraw flag to be set
426    * 
427    * @param b
428    *          value to set redraw to: true = redraw is occurring, false = no
429    *          redraw
430    */
431   public void setRedraw(boolean b)
432   {
433     synchronized (this)
434     {
435       redraw = b;
436     }
437   }
438
439   public void addPropertyChangeListener(RendererListenerI listener)
440   {
441     changeSupport.addPropertyChangeListener(listener);
442   }
443
444   public void removePropertyChangeListener(RendererListenerI listener)
445   {
446     changeSupport.removePropertyChangeListener(listener);
447   }
448 }