JAL-2388 Hidden cols separated from column selection (almost complete)
[jalview.git] / src / jalview / gui / AlignmentPanel.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.gui;
22
23 import jalview.analysis.AnnotationSorter;
24 import jalview.api.AlignViewportI;
25 import jalview.api.AlignmentViewPanel;
26 import jalview.bin.Cache;
27 import jalview.datamodel.AlignmentI;
28 import jalview.datamodel.HiddenColumns;
29 import jalview.datamodel.SearchResultsI;
30 import jalview.datamodel.SequenceFeature;
31 import jalview.datamodel.SequenceGroup;
32 import jalview.datamodel.SequenceI;
33 import jalview.jbgui.GAlignmentPanel;
34 import jalview.math.AlignmentDimension;
35 import jalview.schemes.ResidueProperties;
36 import jalview.structure.StructureSelectionManager;
37 import jalview.util.MessageManager;
38 import jalview.util.Platform;
39 import jalview.viewmodel.ViewportRanges;
40
41 import java.awt.BorderLayout;
42 import java.awt.Color;
43 import java.awt.Container;
44 import java.awt.Dimension;
45 import java.awt.Font;
46 import java.awt.FontMetrics;
47 import java.awt.Graphics;
48 import java.awt.Insets;
49 import java.awt.event.AdjustmentEvent;
50 import java.awt.event.AdjustmentListener;
51 import java.awt.print.PageFormat;
52 import java.awt.print.Printable;
53 import java.awt.print.PrinterException;
54 import java.beans.PropertyChangeEvent;
55 import java.beans.PropertyChangeListener;
56 import java.io.File;
57 import java.io.FileWriter;
58 import java.io.PrintWriter;
59 import java.util.List;
60
61 import javax.swing.SwingUtilities;
62
63 /**
64  * DOCUMENT ME!
65  * 
66  * @author $author$
67  * @version $Revision: 1.161 $
68  */
69 public class AlignmentPanel extends GAlignmentPanel implements
70         AdjustmentListener, Printable, AlignmentViewPanel
71 {
72   public AlignViewport av;
73
74   ViewportRanges vpRanges;
75
76   OverviewPanel overviewPanel;
77
78   private SeqPanel seqPanel;
79
80   private IdPanel idPanel;
81
82   private boolean headless;
83
84   IdwidthAdjuster idwidthAdjuster;
85
86   /** DOCUMENT ME!! */
87   public AlignFrame alignFrame;
88
89   private ScalePanel scalePanel;
90
91   private AnnotationPanel annotationPanel;
92
93   private AnnotationLabels alabels;
94
95   // this value is set false when selection area being dragged
96   boolean fastPaint = true;
97
98   private int hextent = 0;
99
100   private int vextent = 0;
101
102   /*
103    * Flag set while scrolling to follow complementary cDNA/protein scroll. When
104    * true, suppresses invoking the same method recursively.
105    */
106   private boolean dontScrollComplement;
107
108   private PropertyChangeListener propertyChangeListener;
109
110   /**
111    * Creates a new AlignmentPanel object.
112    * 
113    * @param af
114    * @param av
115    */
116   public AlignmentPanel(AlignFrame af, final AlignViewport av)
117   {
118     alignFrame = af;
119     this.av = av;
120     vpRanges = av.getRanges();
121     setSeqPanel(new SeqPanel(av, this));
122     setIdPanel(new IdPanel(av, this));
123
124     setScalePanel(new ScalePanel(av, this));
125
126     idPanelHolder.add(getIdPanel(), BorderLayout.CENTER);
127     idwidthAdjuster = new IdwidthAdjuster(this);
128     idSpaceFillerPanel1.add(idwidthAdjuster, BorderLayout.CENTER);
129
130     setAnnotationPanel(new AnnotationPanel(this));
131     setAlabels(new AnnotationLabels(this));
132
133     annotationScroller.setViewportView(getAnnotationPanel());
134     annotationSpaceFillerHolder.add(getAlabels(), BorderLayout.CENTER);
135
136     scalePanelHolder.add(getScalePanel(), BorderLayout.CENTER);
137     seqPanelHolder.add(getSeqPanel(), BorderLayout.CENTER);
138
139     setScrollValues(0, 0);
140
141     hscroll.addAdjustmentListener(this);
142     vscroll.addAdjustmentListener(this);
143
144     final AlignmentPanel ap = this;
145     propertyChangeListener = new PropertyChangeListener()
146     {
147       @Override
148       public void propertyChange(PropertyChangeEvent evt)
149       {
150         if (evt.getPropertyName().equals("alignment"))
151         {
152           PaintRefresher.Refresh(ap, av.getSequenceSetId(), true, true);
153           alignmentChanged();
154         }
155       }
156     };
157     av.addPropertyChangeListener(propertyChangeListener);
158     fontChanged();
159     adjustAnnotationHeight();
160     updateLayout();
161   }
162
163   @Override
164   public AlignViewportI getAlignViewport()
165   {
166     return av;
167   }
168
169   public void alignmentChanged()
170   {
171     av.alignmentChanged(this);
172
173     alignFrame.updateEditMenuBar();
174
175     paintAlignment(true);
176
177   }
178
179   /**
180    * DOCUMENT ME!
181    */
182   public void fontChanged()
183   {
184     // set idCanvas bufferedImage to null
185     // to prevent drawing old image
186     FontMetrics fm = getFontMetrics(av.getFont());
187
188     scalePanelHolder.setPreferredSize(new Dimension(10, av.getCharHeight()
189             + fm.getDescent()));
190     idSpaceFillerPanel1.setPreferredSize(new Dimension(10, av
191             .getCharHeight() + fm.getDescent()));
192
193     getIdPanel().getIdCanvas().gg = null;
194     getSeqPanel().seqCanvas.img = null;
195     getAnnotationPanel().adjustPanelHeight();
196
197     Dimension d = calculateIdWidth();
198
199     d.setSize(d.width + 4, d.height);
200     getIdPanel().getIdCanvas().setPreferredSize(d);
201     hscrollFillerPanel.setPreferredSize(d);
202
203     if (overviewPanel != null)
204     {
205       overviewPanel.setBoxPosition();
206     }
207     if (this.alignFrame.getSplitViewContainer() != null)
208     {
209       ((SplitFrame) this.alignFrame.getSplitViewContainer()).adjustLayout();
210     }
211
212     repaint();
213   }
214
215   /**
216    * Calculate the width of the alignment labels based on the displayed names
217    * and any bounds on label width set in preferences.
218    * 
219    * @return Dimension giving the maximum width of the alignment label panel
220    *         that should be used.
221    */
222   public Dimension calculateIdWidth()
223   {
224     // calculate sensible default width when no preference is available
225     Dimension r = null;
226     if (av.getIdWidth() < 0)
227     {
228       int afwidth = (alignFrame != null ? alignFrame.getWidth() : 300);
229       int maxwidth = Math.max(20, Math.min(afwidth - 200, 2 * afwidth / 3));
230       r = calculateIdWidth(maxwidth);
231       av.setIdWidth(r.width);
232     }
233     else
234     {
235       r = new Dimension();
236       r.width = av.getIdWidth();
237       r.height = 0;
238     }
239     return r;
240   }
241
242   /**
243    * Calculate the width of the alignment labels based on the displayed names
244    * and any bounds on label width set in preferences.
245    * 
246    * @param maxwidth
247    *          -1 or maximum width allowed for IdWidth
248    * @return Dimension giving the maximum width of the alignment label panel
249    *         that should be used.
250    */
251   public Dimension calculateIdWidth(int maxwidth)
252   {
253     Container c = new Container();
254
255     FontMetrics fm = c.getFontMetrics(new Font(av.font.getName(),
256             Font.ITALIC, av.font.getSize()));
257
258     AlignmentI al = av.getAlignment();
259     int i = 0;
260     int idWidth = 0;
261     String id;
262
263     while ((i < al.getHeight()) && (al.getSequenceAt(i) != null))
264     {
265       SequenceI s = al.getSequenceAt(i);
266
267       id = s.getDisplayId(av.getShowJVSuffix());
268
269       if (fm.stringWidth(id) > idWidth)
270       {
271         idWidth = fm.stringWidth(id);
272       }
273
274       i++;
275     }
276
277     // Also check annotation label widths
278     i = 0;
279
280     if (al.getAlignmentAnnotation() != null)
281     {
282       fm = c.getFontMetrics(getAlabels().getFont());
283
284       while (i < al.getAlignmentAnnotation().length)
285       {
286         String label = al.getAlignmentAnnotation()[i].label;
287
288         if (fm.stringWidth(label) > idWidth)
289         {
290           idWidth = fm.stringWidth(label);
291         }
292
293         i++;
294       }
295     }
296
297     return new Dimension(maxwidth < 0 ? idWidth : Math.min(maxwidth,
298             idWidth), 12);
299   }
300
301   /**
302    * Highlight the given results on the alignment.
303    * 
304    */
305   public void highlightSearchResults(SearchResultsI results)
306   {
307     scrollToPosition(results);
308     getSeqPanel().seqCanvas.highlightSearchResults(results);
309   }
310
311   /**
312    * Scroll the view to show the position of the highlighted region in results
313    * (if any) and redraw the overview
314    * 
315    * @param results
316    */
317   public boolean scrollToPosition(SearchResultsI results)
318   {
319     return scrollToPosition(results, 0, true, false);
320   }
321
322   /**
323    * Scroll the view to show the position of the highlighted region in results
324    * (if any)
325    * 
326    * @param searchResults
327    * @param redrawOverview
328    * @return
329    */
330   public boolean scrollToPosition(SearchResultsI searchResults,
331           boolean redrawOverview)
332   {
333     return scrollToPosition(searchResults, 0, redrawOverview, false);
334   }
335
336   /**
337    * Scroll the view to show the position of the highlighted region in results
338    * (if any)
339    * 
340    * @param results
341    * @param verticalOffset
342    *          if greater than zero, allows scrolling to a position below the
343    *          first displayed sequence
344    * @param redrawOverview
345    *          - when set, the overview will be recalculated (takes longer)
346    * @param centre
347    *          if true, try to centre the search results horizontally in the view
348    * @return false if results were not found
349    */
350   public boolean scrollToPosition(SearchResultsI results,
351           int verticalOffset, boolean redrawOverview, boolean centre)
352   {
353     int startv, endv, starts, ends;
354     // TODO: properly locate search results in view when large numbers of hidden
355     // columns exist before highlighted region
356     // do we need to scroll the panel?
357     // TODO: tons of nullpointerexceptions raised here.
358     if (results != null && results.getSize() > 0 && av != null
359             && av.getAlignment() != null)
360     {
361       int seqIndex = av.getAlignment().findIndex(results);
362       if (seqIndex == -1)
363       {
364         return false;
365       }
366       SequenceI seq = av.getAlignment().getSequenceAt(seqIndex);
367
368       int[] r = results.getResults(seq, 0, av.getAlignment().getWidth());
369       if (r == null)
370       {
371         return false;
372       }
373       int start = r[0];
374       int end = r[1];
375       // DEBUG
376       // System.err.println(this.av.viewName + " Seq : " + seqIndex
377       // + " Scroll to " + start + "," + end);
378
379       /*
380        * To centre results, scroll to positions half the visible width
381        * left/right of the start/end positions
382        */
383       if (centre)
384       {
385         int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2 - 1;
386         start = Math.max(start - offset, 0);
387         end = end + offset - 1;
388       }
389       if (start < 0)
390       {
391         return false;
392       }
393       if (end == seq.getEnd())
394       {
395         return false;
396       }
397       if (av.hasHiddenColumns())
398       {
399         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
400         start = hidden.findColumnPosition(start);
401         end = hidden.findColumnPosition(end);
402         if (start == end)
403         {
404           if (!hidden.isVisible(r[0]))
405           {
406             // don't scroll - position isn't visible
407             return false;
408           }
409         }
410       }
411
412       /*
413        * allow for offset of target sequence (actually scroll to one above it)
414        */
415       seqIndex = Math.max(0, seqIndex - verticalOffset);
416
417       // System.out.println("start=" + start + ", end=" + end + ", startv="
418       // + av.getStartRes() + ", endv=" + av.getEndRes() + ", starts="
419       // + av.getStartSeq() + ", ends=" + av.getEndSeq());
420       if (!av.getWrapAlignment())
421       {
422         if ((startv = vpRanges.getStartRes()) >= start)
423         {
424           /*
425            * Scroll left to make start of search results visible
426            */
427           // setScrollValues(start - 1, seqIndex); // plus one residue
428           setScrollValues(start, seqIndex);
429         }
430         else if ((endv = vpRanges.getEndRes()) <= end)
431         {
432           /*
433            * Scroll right to make end of search results visible
434            */
435           // setScrollValues(startv + 1 + end - endv, seqIndex); // plus one
436           setScrollValues(startv + end - endv, seqIndex);
437         }
438         else if ((starts = vpRanges.getStartSeq()) > seqIndex)
439         {
440           /*
441            * Scroll up to make start of search results visible
442            */
443           setScrollValues(vpRanges.getStartRes(), seqIndex);
444         }
445         else if ((ends = vpRanges.getEndSeq()) <= seqIndex)
446         {
447           /*
448            * Scroll down to make end of search results visible
449            */
450           setScrollValues(vpRanges.getStartRes(), starts + seqIndex - ends
451                   + 1);
452         }
453         /*
454          * Else results are already visible - no need to scroll
455          */
456       }
457       else
458       {
459         scrollToWrappedVisible(start);
460       }
461     }
462     if (redrawOverview && overviewPanel != null)
463     {
464       overviewPanel.setBoxPosition();
465     }
466     paintAlignment(redrawOverview);
467     return true;
468   }
469
470   void scrollToWrappedVisible(int res)
471   {
472     int cwidth = getSeqPanel().seqCanvas
473             .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
474     if (res < vpRanges.getStartRes()
475             || res >= (vpRanges.getStartRes() + cwidth))
476     {
477       vscroll.setValue((res / cwidth));
478       vpRanges.setStartRes(vscroll.getValue() * cwidth);
479     }
480
481   }
482
483   /**
484    * DOCUMENT ME!
485    * 
486    * @return DOCUMENT ME!
487    */
488   public OverviewPanel getOverviewPanel()
489   {
490     return overviewPanel;
491   }
492
493   /**
494    * DOCUMENT ME!
495    * 
496    * @param op
497    *          DOCUMENT ME!
498    */
499   public void setOverviewPanel(OverviewPanel op)
500   {
501     overviewPanel = op;
502   }
503
504   /**
505    * 
506    * @param b
507    *          Hide or show annotation panel
508    * 
509    */
510   public void setAnnotationVisible(boolean b)
511   {
512     if (!av.getWrapAlignment())
513     {
514       annotationSpaceFillerHolder.setVisible(b);
515       annotationScroller.setVisible(b);
516     }
517     repaint();
518   }
519
520   /**
521    * automatically adjust annotation panel height for new annotation whilst
522    * ensuring the alignment is still visible.
523    */
524   @Override
525   public void adjustAnnotationHeight()
526   {
527     // TODO: display vertical annotation scrollbar if necessary
528     // this is called after loading new annotation onto alignment
529     if (alignFrame.getHeight() == 0)
530     {
531       System.out.println("NEEDS FIXING");
532     }
533     validateAnnotationDimensions(true);
534     addNotify();
535     paintAlignment(true);
536   }
537
538   /**
539    * calculate the annotation dimensions and refresh slider values accordingly.
540    * need to do repaints/notifys afterwards.
541    */
542   protected void validateAnnotationDimensions(boolean adjustPanelHeight)
543   {
544     int annotationHeight = getAnnotationPanel().adjustPanelHeight();
545
546     if (adjustPanelHeight)
547     {
548       int rowHeight = av.getCharHeight();
549       int alignmentHeight = rowHeight * av.getAlignment().getHeight();
550
551       /*
552        * Estimate available height in the AlignFrame for alignment +
553        * annotations. Deduct an estimate for title bar, menu bar, scale panel,
554        * hscroll, status bar (as these are not laid out we can't inspect their
555        * actual heights). Insets gives frame borders.
556        */
557       int stuff = Platform.isAMac() ? 80 : 100;
558       Insets insets = alignFrame.getInsets();
559       int availableHeight = alignFrame.getHeight() - stuff - insets.top
560               - insets.bottom;
561
562       /*
563        * If not enough vertical space, maximize annotation height while keeping
564        * at least two rows of alignment visible
565        */
566       if (annotationHeight + alignmentHeight > availableHeight)
567       {
568         annotationHeight = Math.min(annotationHeight, availableHeight - 2
569                 * rowHeight);
570       }
571     }
572     else
573     {
574       // maintain same window layout whilst updating sliders
575       annotationHeight = annotationScroller.getSize().height;
576     }
577     hscroll.addNotify();
578
579     annotationScroller.setPreferredSize(new Dimension(annotationScroller
580             .getWidth(), annotationHeight));
581
582     Dimension e = idPanel.getSize();
583     alabels.setSize(new Dimension(e.width, annotationHeight));
584
585     annotationSpaceFillerHolder.setPreferredSize(new Dimension(
586             annotationSpaceFillerHolder.getWidth(), annotationHeight));
587     annotationScroller.validate();
588     annotationScroller.addNotify();
589   }
590
591   /**
592    * update alignment layout for viewport settings
593    * 
594    * @param wrap
595    *          DOCUMENT ME!
596    */
597   public void updateLayout()
598   {
599     fontChanged();
600     setAnnotationVisible(av.isShowAnnotation());
601     boolean wrap = av.getWrapAlignment();
602     vpRanges.setStartSeq(0);
603     scalePanelHolder.setVisible(!wrap);
604     hscroll.setVisible(!wrap);
605     idwidthAdjuster.setVisible(!wrap);
606
607     if (wrap)
608     {
609       annotationScroller.setVisible(false);
610       annotationSpaceFillerHolder.setVisible(false);
611     }
612     else if (av.isShowAnnotation())
613     {
614       annotationScroller.setVisible(true);
615       annotationSpaceFillerHolder.setVisible(true);
616     }
617
618     idSpaceFillerPanel1.setVisible(!wrap);
619
620     repaint();
621   }
622
623   // return value is true if the scroll is valid
624   public boolean scrollUp(boolean up)
625   {
626     if (up)
627     {
628       if (vscroll.getValue() < 1)
629       {
630         return false;
631       }
632
633       fastPaint = false;
634       vscroll.setValue(vscroll.getValue() - 1);
635     }
636     else
637     {
638       if ((vextent + vscroll.getValue()) >= av.getAlignment().getHeight())
639       {
640         return false;
641       }
642
643       fastPaint = false;
644       vscroll.setValue(vscroll.getValue() + 1);
645     }
646
647     fastPaint = true;
648
649     return true;
650   }
651
652   /**
653    * DOCUMENT ME!
654    * 
655    * @param right
656    *          DOCUMENT ME!
657    * 
658    * @return DOCUMENT ME!
659    */
660   public boolean scrollRight(boolean right)
661   {
662     if (!right)
663     {
664       if (hscroll.getValue() < 1)
665       {
666         return false;
667       }
668
669       fastPaint = false;
670       hscroll.setValue(hscroll.getValue() - 1);
671     }
672     else
673     {
674       if ((hextent + hscroll.getValue()) >= av.getAlignment().getWidth())
675       {
676         return false;
677       }
678
679       fastPaint = false;
680       hscroll.setValue(hscroll.getValue() + 1);
681     }
682
683     fastPaint = true;
684
685     return true;
686   }
687
688   /**
689    * Adjust row/column scrollers to show a visible position in the alignment.
690    * 
691    * @param x
692    *          visible column to scroll to
693    * @param y
694    *          visible row to scroll to
695    * 
696    */
697   public void setScrollValues(int x, int y)
698   {
699     if (av == null || av.getAlignment() == null)
700     {
701       return;
702     }
703     int width = av.getAlignment().getWidth();
704     int height = av.getAlignment().getHeight();
705
706     if (av.hasHiddenColumns())
707     {
708       // reset the width to exclude hidden columns
709       width = av.getAlignment().getHiddenColumns()
710               .findColumnPosition(width);
711     }
712
713     hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
714     vextent = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
715
716     if (hextent > width)
717     {
718       hextent = width;
719     }
720
721     if (vextent > height)
722     {
723       vextent = height;
724     }
725
726     if ((hextent + x) > width)
727     {
728       x = width - hextent;
729     }
730
731     if ((vextent + y) > height)
732     {
733       y = height - vextent;
734     }
735
736     if (y < 0)
737     {
738       y = 0;
739     }
740
741     if (x < 0)
742     {
743       x = 0;
744     }
745
746     // update endRes after x has (possibly) been adjusted
747     vpRanges.setEndRes((x + (getSeqPanel().seqCanvas.getWidth() / av
748             .getCharWidth())) - 1);
749
750     /*
751      * each scroll adjustment triggers adjustmentValueChanged, which resets the
752      * 'do not scroll complement' flag; ensure it is the same for both
753      * operations
754      */
755     boolean flag = isDontScrollComplement();
756     hscroll.setValues(x, hextent, 0, width);
757     setDontScrollComplement(flag);
758     vscroll.setValues(y, vextent, 0, height);
759   }
760
761   /**
762    * DOCUMENT ME!
763    * 
764    * @param evt
765    *          DOCUMENT ME!
766    */
767   @Override
768   public void adjustmentValueChanged(AdjustmentEvent evt)
769   {
770     int oldX = vpRanges.getStartRes();
771     int oldY = vpRanges.getStartSeq();
772
773     if (evt.getSource() == hscroll)
774     {
775       int x = hscroll.getValue();
776       vpRanges.setStartRes(x);
777       vpRanges.setEndRes((x + (getSeqPanel().seqCanvas.getWidth() / av
778               .getCharWidth())) - 1);
779     }
780
781     if (evt.getSource() == vscroll)
782     {
783       int offy = vscroll.getValue();
784
785       if (av.getWrapAlignment())
786       {
787         if (offy > -1)
788         {
789           int rowSize = getSeqPanel().seqCanvas
790                   .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
791           vpRanges.setStartRes(offy * rowSize);
792           vpRanges.setEndRes((offy + 1) * rowSize);
793         }
794         else
795         {
796           // This is only called if file loaded is a jar file that
797           // was wrapped when saved and user has wrap alignment true
798           // as preference setting
799           SwingUtilities.invokeLater(new Runnable()
800           {
801             @Override
802             public void run()
803             {
804               setScrollValues(vpRanges.getStartRes(),
805                       vpRanges.getStartSeq());
806             }
807           });
808         }
809       }
810       else
811       {
812         vpRanges.setStartSeq(offy);
813         vpRanges.setEndSeq(offy
814                 + (getSeqPanel().seqCanvas.getHeight() / av.getCharHeight())
815                 - 1);
816       }
817     }
818
819     if (overviewPanel != null)
820     {
821       overviewPanel.setBoxPosition();
822     }
823
824     int scrollX = vpRanges.getStartRes() - oldX;
825     int scrollY = vpRanges.getStartSeq() - oldY;
826
827     if (av.getWrapAlignment() || !fastPaint)
828     {
829       repaint();
830     }
831     else
832     {
833       // Make sure we're not trying to draw a panel
834       // larger than the visible window
835       if (scrollX > vpRanges.getEndRes() - vpRanges.getStartRes())
836       {
837         scrollX = vpRanges.getEndRes() - vpRanges.getStartRes();
838       }
839       else if (scrollX < vpRanges.getStartRes() - vpRanges.getEndRes())
840       {
841         scrollX = vpRanges.getStartRes() - vpRanges.getEndRes();
842       }
843
844       if (scrollX != 0 || scrollY != 0)
845       {
846         getIdPanel().getIdCanvas().fastPaint(scrollY);
847         getSeqPanel().seqCanvas.fastPaint(scrollX, scrollY);
848         getScalePanel().repaint();
849
850         if (av.isShowAnnotation() && scrollX != 0)
851         {
852           getAnnotationPanel().fastPaint(scrollX);
853         }
854       }
855     }
856     /*
857      * If there is one, scroll the (Protein/cDNA) complementary alignment to
858      * match, unless we are ourselves doing that.
859      */
860     if (isDontScrollComplement())
861     {
862       setDontScrollComplement(false);
863     }
864     else
865     {
866       av.scrollComplementaryAlignment();
867     }
868   }
869
870   /**
871    * Repaint the alignment including the annotations and overview panels (if
872    * shown).
873    */
874   @Override
875   public void paintAlignment(boolean updateOverview)
876   {
877     final AnnotationSorter sorter = new AnnotationSorter(getAlignment(),
878             av.isShowAutocalculatedAbove());
879     sorter.sort(getAlignment().getAlignmentAnnotation(),
880             av.getSortAnnotationsBy());
881     repaint();
882
883     if (updateOverview)
884     {
885       // TODO: determine if this paintAlignment changed structure colours
886       av.getStructureSelectionManager().sequenceColoursChanged(this);
887
888       if (overviewPanel != null)
889       {
890         overviewPanel.updateOverviewImage();
891       }
892     }
893   }
894
895   /**
896    * DOCUMENT ME!
897    * 
898    * @param g
899    *          DOCUMENT ME!
900    */
901   @Override
902   public void paintComponent(Graphics g)
903   {
904     invalidate();
905
906     Dimension d = getIdPanel().getIdCanvas().getPreferredSize();
907     idPanelHolder.setPreferredSize(d);
908     hscrollFillerPanel.setPreferredSize(new Dimension(d.width, 12));
909     validate();
910
911     /*
912      * set scroll bar positions; first suppress this being 'followed' in any
913      * complementary split pane
914      */
915     setDontScrollComplement(true);
916
917     if (av.getWrapAlignment())
918     {
919       int maxwidth = av.getAlignment().getWidth();
920
921       if (av.hasHiddenColumns())
922       {
923         maxwidth = av.getAlignment().getHiddenColumns()
924                 .findColumnPosition(maxwidth) - 1;
925       }
926
927       int canvasWidth = getSeqPanel().seqCanvas
928               .getWrappedCanvasWidth(getSeqPanel().seqCanvas.getWidth());
929       if (canvasWidth > 0)
930       {
931         int max = maxwidth
932                 / getSeqPanel().seqCanvas
933                         .getWrappedCanvasWidth(getSeqPanel().seqCanvas
934                                 .getWidth()) + 1;
935         vscroll.setMaximum(max);
936         vscroll.setUnitIncrement(1);
937         vscroll.setVisibleAmount(1);
938       }
939     }
940     else
941     {
942       setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
943     }
944   }
945
946   /**
947    * DOCUMENT ME!
948    * 
949    * @param pg
950    *          DOCUMENT ME!
951    * @param pf
952    *          DOCUMENT ME!
953    * @param pi
954    *          DOCUMENT ME!
955    * 
956    * @return DOCUMENT ME!
957    * 
958    * @throws PrinterException
959    *           DOCUMENT ME!
960    */
961   @Override
962   public int print(Graphics pg, PageFormat pf, int pi)
963           throws PrinterException
964   {
965     pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
966
967     int pwidth = (int) pf.getImageableWidth();
968     int pheight = (int) pf.getImageableHeight();
969
970     if (av.getWrapAlignment())
971     {
972       return printWrappedAlignment(pwidth, pheight, pi, pg);
973     }
974     else
975     {
976       return printUnwrapped(pwidth, pheight, pi, pg, pg);
977     }
978   }
979
980   /**
981    * DOCUMENT ME!
982    * 
983    * @param pg
984    *          DOCUMENT ME!
985    * @param pwidth
986    *          DOCUMENT ME!
987    * @param pheight
988    *          DOCUMENT ME!
989    * @param pi
990    *          DOCUMENT ME!
991    * 
992    * @return DOCUMENT ME!
993    * 
994    * @throws PrinterException
995    *           DOCUMENT ME!
996    */
997   /**
998    * Draws the alignment image, including sequence ids, sequences, and
999    * annotation labels and annotations if shown, on either one or two Graphics
1000    * context.
1001    * 
1002    * @param pageWidth
1003    * @param pageHeight
1004    * @param pi
1005    * @param idGraphics
1006    *          the graphics context for sequence ids and annotation labels
1007    * @param alignmentGraphics
1008    *          the graphics context for sequences and annotations (may or may not
1009    *          be the same context as idGraphics)
1010    * @return
1011    * @throws PrinterException
1012    */
1013   public int printUnwrapped(int pageWidth, int pageHeight, int pi,
1014           Graphics idGraphics, Graphics alignmentGraphics)
1015           throws PrinterException
1016   {
1017     final int idWidth = getVisibleIdWidth(false);
1018
1019     /*
1020      * Get the horizontal offset to where we draw the sequences.
1021      * This is idWidth if using a single Graphics context, else zero.
1022      */
1023     final int alignmentGraphicsOffset = idGraphics != alignmentGraphics ? 0 : idWidth;
1024
1025     FontMetrics fm = getFontMetrics(av.getFont());
1026     int charHeight = av.getCharHeight();
1027     int scaleHeight = charHeight + fm.getDescent();
1028
1029     idGraphics.setColor(Color.white);
1030     idGraphics.fillRect(0, 0, pageWidth, pageHeight);
1031     idGraphics.setFont(av.getFont());
1032
1033     /*
1034      * How many sequences and residues can we fit on a printable page?
1035      */
1036     int totalRes = (pageWidth - idWidth) / av.getCharWidth();
1037
1038     int totalSeq = (pageHeight - scaleHeight) / charHeight - 1;
1039
1040     int alignmentWidth = av.getAlignment().getWidth();
1041     int pagesWide = (alignmentWidth / totalRes) + 1;
1042
1043     final int startRes = (pi % pagesWide) * totalRes;
1044     int endRes = (startRes + totalRes) - 1;
1045
1046     if (endRes > (alignmentWidth - 1))
1047     {
1048       endRes = alignmentWidth - 1;
1049     }
1050
1051     final int startSeq = (pi / pagesWide) * totalSeq;
1052     int endSeq = startSeq + totalSeq;
1053
1054     int alignmentHeight = av.getAlignment().getHeight();
1055     if (endSeq > alignmentHeight)
1056     {
1057       endSeq = alignmentHeight;
1058     }
1059
1060     int pagesHigh = ((alignmentHeight / totalSeq) + 1)
1061             * pageHeight;
1062
1063     if (av.isShowAnnotation())
1064     {
1065       pagesHigh += getAnnotationPanel().adjustPanelHeight() + 3;
1066     }
1067
1068     pagesHigh /= pageHeight;
1069
1070     if (pi >= (pagesWide * pagesHigh))
1071     {
1072       return Printable.NO_SUCH_PAGE;
1073     }
1074     final int alignmentDrawnHeight = (endSeq - startSeq) * charHeight
1075             + 3;
1076
1077     /*
1078      * draw the Scale at horizontal offset, then reset to top left (0, 0)
1079      */
1080     alignmentGraphics.translate(alignmentGraphicsOffset, 0);
1081     getScalePanel().drawScale(alignmentGraphics, startRes, endRes,
1082             pageWidth - idWidth, scaleHeight);
1083     alignmentGraphics.translate(-alignmentGraphicsOffset, 0);
1084
1085     /*
1086      * Draw the sequence ids, offset for scale height,
1087      * then reset to top left (0, 0)
1088      */
1089     idGraphics.translate(0, scaleHeight);
1090     idGraphics.setFont(getIdPanel().getIdCanvas().getIdfont());
1091     Color currentColor = null;
1092     Color currentTextColor = null;
1093
1094     SequenceI seq;
1095     for (int i = startSeq; i < endSeq; i++)
1096     {
1097       seq = av.getAlignment().getSequenceAt(i);
1098       if ((av.getSelectionGroup() != null)
1099               && av.getSelectionGroup().getSequences(null).contains(seq))
1100       {
1101         /*
1102          * gray out ids of sequences in selection group (if any)
1103          */
1104         currentColor = Color.gray;
1105         currentTextColor = Color.black;
1106       }
1107       else
1108       {
1109         currentColor = av.getSequenceColour(seq);
1110         currentTextColor = Color.black;
1111       }
1112
1113       idGraphics.setColor(currentColor);
1114       idGraphics.fillRect(0, (i - startSeq) * charHeight, idWidth,
1115               charHeight);
1116
1117       idGraphics.setColor(currentTextColor);
1118
1119       int xPos = 0;
1120       String displayId = seq.getDisplayId(av.getShowJVSuffix());
1121       if (av.isRightAlignIds())
1122       {
1123         fm = idGraphics.getFontMetrics();
1124         xPos = idWidth
1125                 - fm.stringWidth(displayId)
1126                 - 4;
1127       }
1128
1129       idGraphics.drawString(displayId, xPos,
1130               (((i - startSeq) * charHeight) + charHeight)
1131                       - (charHeight / 5));
1132     }
1133     idGraphics.setFont(av.getFont());
1134     idGraphics.translate(0, -scaleHeight);
1135
1136     /*
1137      * draw the sequences, offset for scale height, and id width (if using a
1138      * single graphics context), then reset to (0, scale height)
1139      */
1140     alignmentGraphics.translate(alignmentGraphicsOffset, scaleHeight);
1141     getSeqPanel().seqCanvas.drawPanel(alignmentGraphics, startRes, endRes,
1142             startSeq, endSeq, 0);
1143     alignmentGraphics.translate(-alignmentGraphicsOffset, 0);
1144
1145     if (av.isShowAnnotation() && (endSeq == alignmentHeight))
1146     {
1147       /*
1148        * draw annotation labels; drawComponent() translates by
1149        * getScrollOffset(), so compensate for that first;
1150        * then reset to (0, scale height)
1151        */
1152       int offset = getAlabels().getScrollOffset();
1153       idGraphics.translate(0, -offset);
1154       idGraphics.translate(0, alignmentDrawnHeight);
1155       getAlabels().drawComponent(idGraphics, idWidth);
1156       idGraphics.translate(0, -alignmentDrawnHeight);
1157
1158       /*
1159        * draw the annotations starting at 
1160        * (idOffset, alignmentHeight) from (0, scaleHeight)
1161        */
1162       alignmentGraphics.translate(alignmentGraphicsOffset, alignmentDrawnHeight);
1163       getAnnotationPanel().renderer.drawComponent(getAnnotationPanel(), av,
1164               alignmentGraphics, -1, startRes, endRes + 1);
1165     }
1166
1167     return Printable.PAGE_EXISTS;
1168   }
1169
1170   /**
1171    * DOCUMENT ME!
1172    * 
1173    * @param pg
1174    *          DOCUMENT ME!
1175    * @param pwidth
1176    *          DOCUMENT ME!
1177    * @param pheight
1178    *          DOCUMENT ME!
1179    * @param pi
1180    *          DOCUMENT ME!
1181    * 
1182    * @return DOCUMENT ME!
1183    * 
1184    * @throws PrinterException
1185    *           DOCUMENT ME!
1186    */
1187   public int printWrappedAlignment(int pwidth, int pheight, int pi,
1188           Graphics pg) throws PrinterException
1189   {
1190     int annotationHeight = 0;
1191     AnnotationLabels labels = null;
1192     if (av.isShowAnnotation())
1193     {
1194       annotationHeight = getAnnotationPanel().adjustPanelHeight();
1195       labels = new AnnotationLabels(av);
1196     }
1197
1198     int hgap = av.getCharHeight();
1199     if (av.getScaleAboveWrapped())
1200     {
1201       hgap += av.getCharHeight();
1202     }
1203
1204     int cHeight = av.getAlignment().getHeight() * av.getCharHeight() + hgap
1205             + annotationHeight;
1206
1207     int idWidth = getVisibleIdWidth(false);
1208
1209     int maxwidth = av.getAlignment().getWidth();
1210     if (av.hasHiddenColumns())
1211     {
1212       maxwidth = av.getAlignment().getHiddenColumns()
1213               .findColumnPosition(maxwidth) - 1;
1214     }
1215
1216     int resWidth = getSeqPanel().seqCanvas.getWrappedCanvasWidth(pwidth
1217             - idWidth);
1218
1219     int totalHeight = cHeight * (maxwidth / resWidth + 1);
1220
1221     pg.setColor(Color.white);
1222     pg.fillRect(0, 0, pwidth, pheight);
1223     pg.setFont(av.getFont());
1224
1225     // //////////////
1226     // Draw the ids
1227     pg.setColor(Color.black);
1228
1229     pg.translate(0, -pi * pheight);
1230
1231     pg.setClip(0, pi * pheight, pwidth, pheight);
1232
1233     int ypos = hgap;
1234
1235     do
1236     {
1237       for (int i = 0; i < av.getAlignment().getHeight(); i++)
1238       {
1239         pg.setFont(getIdPanel().getIdCanvas().getIdfont());
1240         SequenceI s = av.getAlignment().getSequenceAt(i);
1241         String string = s.getDisplayId(av.getShowJVSuffix());
1242         int xPos = 0;
1243         if (av.isRightAlignIds())
1244         {
1245           FontMetrics fm = pg.getFontMetrics();
1246           xPos = idWidth - fm.stringWidth(string) - 4;
1247         }
1248         pg.drawString(string, xPos,
1249                 ((i * av.getCharHeight()) + ypos + av.getCharHeight())
1250                         - (av.getCharHeight() / 5));
1251       }
1252       if (labels != null)
1253       {
1254         pg.translate(-3,
1255                 ypos + (av.getAlignment().getHeight() * av.getCharHeight()));
1256
1257         pg.setFont(av.getFont());
1258         labels.drawComponent(pg, idWidth);
1259         pg.translate(
1260                 +3,
1261                 -ypos
1262                         - (av.getAlignment().getHeight() * av
1263                                 .getCharHeight()));
1264       }
1265
1266       ypos += cHeight;
1267     } while (ypos < totalHeight);
1268
1269     pg.translate(idWidth, 0);
1270
1271     getSeqPanel().seqCanvas.drawWrappedPanel(pg, pwidth - idWidth,
1272             totalHeight, 0);
1273
1274     if ((pi * pheight) < totalHeight)
1275     {
1276       return Printable.PAGE_EXISTS;
1277
1278     }
1279     else
1280     {
1281       return Printable.NO_SUCH_PAGE;
1282     }
1283   }
1284
1285   /**
1286    * get current sequence ID panel width, or nominal value if panel were to be
1287    * displayed using default settings
1288    * 
1289    * @return
1290    */
1291   public int getVisibleIdWidth()
1292   {
1293     return getVisibleIdWidth(true);
1294   }
1295
1296   /**
1297    * get current sequence ID panel width, or nominal value if panel were to be
1298    * displayed using default settings
1299    * 
1300    * @param onscreen
1301    *          indicate if the Id width for onscreen or offscreen display should
1302    *          be returned
1303    * @return
1304    */
1305   public int getVisibleIdWidth(boolean onscreen)
1306   {
1307     // see if rendering offscreen - check preferences and calc width accordingly
1308     if (!onscreen && Cache.getDefault("FIGURE_AUTOIDWIDTH", false))
1309     {
1310       return calculateIdWidth(-1).width + 4;
1311     }
1312     Integer idwidth = null;
1313     if (onscreen
1314             || (idwidth = Cache.getIntegerProperty("FIGURE_FIXEDIDWIDTH")) == null)
1315     {
1316       int w = getIdPanel().getWidth();
1317       return (w > 0 ? w : calculateIdWidth().width + 4);
1318     }
1319     return idwidth.intValue() + 4;
1320   }
1321
1322   void makeAlignmentImage(jalview.util.ImageMaker.TYPE type, File file)
1323   {
1324     int boarderBottomOffset = 5;
1325     long pSessionId = System.currentTimeMillis();
1326     headless = (System.getProperty("java.awt.headless") != null && System
1327             .getProperty("java.awt.headless").equals("true"));
1328     if (alignFrame != null && !headless)
1329     {
1330       if (file != null)
1331       {
1332         alignFrame.setProgressBar(MessageManager.formatMessage(
1333                 "status.saving_file", new Object[] { type.getLabel() }),
1334                 pSessionId);
1335       }
1336     }
1337     try
1338     {
1339       AlignmentDimension aDimension = getAlignmentDimension();
1340       try
1341       {
1342         jalview.util.ImageMaker im;
1343         final String imageAction, imageTitle;
1344         if (type == jalview.util.ImageMaker.TYPE.PNG)
1345         {
1346           imageAction = "Create PNG image from alignment";
1347           imageTitle = null;
1348         }
1349         else if (type == jalview.util.ImageMaker.TYPE.EPS)
1350         {
1351           imageAction = "Create EPS file from alignment";
1352           imageTitle = alignFrame.getTitle();
1353         }
1354         else
1355         {
1356           imageAction = "Create SVG file from alignment";
1357           imageTitle = alignFrame.getTitle();
1358         }
1359
1360         im = new jalview.util.ImageMaker(this, type, imageAction,
1361                 aDimension.getWidth(), aDimension.getHeight()
1362                         + boarderBottomOffset, file, imageTitle,
1363                 alignFrame, pSessionId, headless);
1364         Graphics graphics = im.getGraphics();
1365         if (av.getWrapAlignment())
1366         {
1367           if (graphics != null)
1368           {
1369             printWrappedAlignment(aDimension.getWidth(),
1370                     aDimension.getHeight() + boarderBottomOffset, 0,
1371                     graphics);
1372             im.writeImage();
1373           }
1374         }
1375         else
1376         {
1377           if (graphics != null)
1378           {
1379             printUnwrapped(aDimension.getWidth(), aDimension.getHeight(),
1380                     0, graphics, graphics);
1381             im.writeImage();
1382           }
1383         }
1384
1385       } catch (OutOfMemoryError err)
1386       {
1387         // Be noisy here.
1388         System.out.println("########################\n" + "OUT OF MEMORY "
1389                 + file + "\n" + "########################");
1390         new OOMWarning("Creating Image for " + file, err);
1391         // System.out.println("Create IMAGE: " + err);
1392       } catch (Exception ex)
1393       {
1394         ex.printStackTrace();
1395       }
1396     } finally
1397     {
1398
1399     }
1400   }
1401
1402   public AlignmentDimension getAlignmentDimension()
1403   {
1404     int maxwidth = av.getAlignment().getWidth();
1405     if (av.hasHiddenColumns())
1406     {
1407       maxwidth = av.getAlignment().getHiddenColumns()
1408               .findColumnPosition(maxwidth);
1409     }
1410
1411     int height = ((av.getAlignment().getHeight() + 1) * av.getCharHeight())
1412             + getScalePanel().getHeight();
1413     int width = getVisibleIdWidth(false) + (maxwidth * av.getCharWidth());
1414
1415     if (av.getWrapAlignment())
1416     {
1417       height = getWrappedHeight();
1418       if (headless)
1419       {
1420         // need to obtain default alignment width and then add in any
1421         // additional allowance for id margin
1422         // this duplicates the calculation in getWrappedHeight but adjusts for
1423         // offscreen idWith
1424         width = alignFrame.getWidth() - vscroll.getPreferredSize().width
1425                 - alignFrame.getInsets().left
1426                 - alignFrame.getInsets().right - getVisibleIdWidth()
1427                 + getVisibleIdWidth(false);
1428       }
1429       else
1430       {
1431         width = getSeqPanel().getWidth() + getVisibleIdWidth(false);
1432       }
1433
1434     }
1435     else if (av.isShowAnnotation())
1436     {
1437       height += getAnnotationPanel().adjustPanelHeight() + 3;
1438     }
1439     return new AlignmentDimension(width, height);
1440
1441   }
1442
1443   /**
1444    * DOCUMENT ME!
1445    */
1446   public void makeEPS(File epsFile)
1447   {
1448     makeAlignmentImage(jalview.util.ImageMaker.TYPE.EPS, epsFile);
1449   }
1450
1451   /**
1452    * DOCUMENT ME!
1453    */
1454   public void makePNG(File pngFile)
1455   {
1456     makeAlignmentImage(jalview.util.ImageMaker.TYPE.PNG, pngFile);
1457   }
1458
1459   public void makeSVG(File svgFile)
1460   {
1461     makeAlignmentImage(jalview.util.ImageMaker.TYPE.SVG, svgFile);
1462   }
1463
1464   public void makePNGImageMap(File imgMapFile, String imageName)
1465   {
1466     // /////ONLY WORKS WITH NON WRAPPED ALIGNMENTS
1467     // ////////////////////////////////////////////
1468     int idWidth = getVisibleIdWidth(false);
1469     FontMetrics fm = getFontMetrics(av.getFont());
1470     int scaleHeight = av.getCharHeight() + fm.getDescent();
1471
1472     // Gen image map
1473     // ////////////////////////////////
1474     if (imgMapFile != null)
1475     {
1476       try
1477       {
1478         int s, sSize = av.getAlignment().getHeight(), res, alwidth = av
1479                 .getAlignment().getWidth(), g, gSize, f, fSize, sy;
1480         PrintWriter out = new PrintWriter(new FileWriter(imgMapFile));
1481         out.println(jalview.io.HTMLOutput.getImageMapHTML());
1482         out.println("<img src=\"" + imageName
1483                 + "\" border=\"0\" usemap=\"#Map\" >"
1484                 + "<map name=\"Map\">");
1485
1486         for (s = 0; s < sSize; s++)
1487         {
1488           sy = s * av.getCharHeight() + scaleHeight;
1489
1490           SequenceI seq = av.getAlignment().getSequenceAt(s);
1491           SequenceFeature[] features = seq.getSequenceFeatures();
1492           SequenceGroup[] groups = av.getAlignment().findAllGroups(seq);
1493           for (res = 0; res < alwidth; res++)
1494           {
1495             StringBuilder text = new StringBuilder();
1496             String triplet = null;
1497             if (av.getAlignment().isNucleotide())
1498             {
1499               triplet = ResidueProperties.nucleotideName.get(seq
1500                       .getCharAt(res) + "");
1501             }
1502             else
1503             {
1504               triplet = ResidueProperties.aa2Triplet.get(seq.getCharAt(res)
1505                       + "");
1506             }
1507
1508             if (triplet == null)
1509             {
1510               continue;
1511             }
1512
1513             int alIndex = seq.findPosition(res);
1514             gSize = groups.length;
1515             for (g = 0; g < gSize; g++)
1516             {
1517               if (text.length() < 1)
1518               {
1519                 text.append("<area shape=\"rect\" coords=\"")
1520                         .append((idWidth + res * av.getCharWidth()))
1521                         .append(",").append(sy).append(",")
1522                         .append((idWidth + (res + 1) * av.getCharWidth()))
1523                         .append(",").append((av.getCharHeight() + sy))
1524                         .append("\"").append(" onMouseOver=\"toolTip('")
1525                         .append(alIndex).append(" ").append(triplet);
1526               }
1527
1528               if (groups[g].getStartRes() < res
1529                       && groups[g].getEndRes() > res)
1530               {
1531                 text.append("<br><em>").append(groups[g].getName())
1532                         .append("</em>");
1533               }
1534             }
1535
1536             if (features != null)
1537             {
1538               if (text.length() < 1)
1539               {
1540                 text.append("<area shape=\"rect\" coords=\"")
1541                         .append((idWidth + res * av.getCharWidth()))
1542                         .append(",").append(sy).append(",")
1543                         .append((idWidth + (res + 1) * av.getCharWidth()))
1544                         .append(",").append((av.getCharHeight() + sy))
1545                         .append("\"").append(" onMouseOver=\"toolTip('")
1546                         .append(alIndex).append(" ").append(triplet);
1547               }
1548               fSize = features.length;
1549               for (f = 0; f < fSize; f++)
1550               {
1551
1552                 if ((features[f].getBegin() <= seq.findPosition(res))
1553                         && (features[f].getEnd() >= seq.findPosition(res)))
1554                 {
1555                   if (features[f].isContactFeature())
1556                   {
1557                     if (features[f].getBegin() == seq.findPosition(res)
1558                             || features[f].getEnd() == seq
1559                                     .findPosition(res))
1560                     {
1561                       text.append("<br>").append(features[f].getType())
1562                               .append(" ").append(features[f].getBegin())
1563                               .append(":").append(features[f].getEnd());
1564                     }
1565                   }
1566                   else
1567                   {
1568                     text.append("<br>");
1569                     text.append(features[f].getType());
1570                     if (features[f].getDescription() != null
1571                             && !features[f].getType().equals(
1572                                     features[f].getDescription()))
1573                     {
1574                       text.append(" ").append(features[f].getDescription());
1575                     }
1576
1577                     if (features[f].getValue("status") != null)
1578                     {
1579                       text.append(" (").append(features[f].getValue("status"))
1580                               .append(")");
1581                     }
1582                   }
1583                 }
1584
1585               }
1586             }
1587             if (text.length() > 1)
1588             {
1589               text.append("')\"; onMouseOut=\"toolTip()\";  href=\"#\">");
1590               out.println(text.toString());
1591             }
1592           }
1593         }
1594         out.println("</map></body></html>");
1595         out.close();
1596
1597       } catch (Exception ex)
1598       {
1599         ex.printStackTrace();
1600       }
1601     } // /////////END OF IMAGE MAP
1602
1603   }
1604
1605   int getWrappedHeight()
1606   {
1607     int seqPanelWidth = getSeqPanel().seqCanvas.getWidth();
1608
1609     if (System.getProperty("java.awt.headless") != null
1610             && System.getProperty("java.awt.headless").equals("true"))
1611     {
1612       seqPanelWidth = alignFrame.getWidth() - getVisibleIdWidth()
1613               - vscroll.getPreferredSize().width
1614               - alignFrame.getInsets().left - alignFrame.getInsets().right;
1615     }
1616
1617     int chunkWidth = getSeqPanel().seqCanvas
1618             .getWrappedCanvasWidth(seqPanelWidth);
1619
1620     int hgap = av.getCharHeight();
1621     if (av.getScaleAboveWrapped())
1622     {
1623       hgap += av.getCharHeight();
1624     }
1625
1626     int annotationHeight = 0;
1627     if (av.isShowAnnotation())
1628     {
1629       annotationHeight = getAnnotationPanel().adjustPanelHeight();
1630     }
1631
1632     int cHeight = av.getAlignment().getHeight() * av.getCharHeight() + hgap
1633             + annotationHeight;
1634
1635     int maxwidth = av.getAlignment().getWidth();
1636     if (av.hasHiddenColumns())
1637     {
1638       maxwidth = av.getAlignment().getHiddenColumns()
1639               .findColumnPosition(maxwidth) - 1;
1640     }
1641
1642     int height = ((maxwidth / chunkWidth) + 1) * cHeight;
1643
1644     return height;
1645   }
1646
1647   /**
1648    * close the panel - deregisters all listeners and nulls any references to
1649    * alignment data.
1650    */
1651   public void closePanel()
1652   {
1653     PaintRefresher.RemoveComponent(getSeqPanel().seqCanvas);
1654     PaintRefresher.RemoveComponent(getIdPanel().getIdCanvas());
1655     PaintRefresher.RemoveComponent(this);
1656
1657     /*
1658      * try to ensure references are nulled
1659      */
1660     if (annotationPanel != null)
1661     {
1662       annotationPanel.dispose();
1663     }
1664
1665     if (av != null)
1666     {
1667       av.removePropertyChangeListener(propertyChangeListener);
1668       jalview.structure.StructureSelectionManager ssm = av
1669               .getStructureSelectionManager();
1670       ssm.removeStructureViewerListener(getSeqPanel(), null);
1671       ssm.removeSelectionListener(getSeqPanel());
1672       ssm.removeCommandListener(av);
1673       ssm.removeStructureViewerListener(getSeqPanel(), null);
1674       ssm.removeSelectionListener(getSeqPanel());
1675       av.dispose();
1676       av = null;
1677     }
1678     else
1679     {
1680       if (Cache.log.isDebugEnabled())
1681       {
1682         Cache.log.warn("Closing alignment panel which is already closed.");
1683       }
1684     }
1685   }
1686
1687   /**
1688    * hides or shows dynamic annotation rows based on groups and av state flags
1689    */
1690   public void updateAnnotation()
1691   {
1692     updateAnnotation(false, false);
1693   }
1694
1695   public void updateAnnotation(boolean applyGlobalSettings)
1696   {
1697     updateAnnotation(applyGlobalSettings, false);
1698   }
1699
1700   public void updateAnnotation(boolean applyGlobalSettings,
1701           boolean preserveNewGroupSettings)
1702   {
1703     av.updateGroupAnnotationSettings(applyGlobalSettings,
1704             preserveNewGroupSettings);
1705     adjustAnnotationHeight();
1706   }
1707
1708   @Override
1709   public AlignmentI getAlignment()
1710   {
1711     return av == null ? null : av.getAlignment();
1712   }
1713
1714   @Override
1715   public String getViewName()
1716   {
1717     return av.viewName;
1718   }
1719
1720   /**
1721    * Make/Unmake this alignment panel the current input focus
1722    * 
1723    * @param b
1724    */
1725   public void setSelected(boolean b)
1726   {
1727     try
1728     {
1729       if (alignFrame.getSplitViewContainer() != null)
1730       {
1731         /*
1732          * bring enclosing SplitFrame to front first if there is one
1733          */
1734         ((SplitFrame) alignFrame.getSplitViewContainer()).setSelected(b);
1735       }
1736       alignFrame.setSelected(b);
1737     } catch (Exception ex)
1738     {
1739     }
1740
1741     if (b)
1742     {
1743       alignFrame.setDisplayedView(this);
1744     }
1745   }
1746
1747   @Override
1748   public StructureSelectionManager getStructureSelectionManager()
1749   {
1750     return av.getStructureSelectionManager();
1751   }
1752
1753   @Override
1754   public void raiseOOMWarning(String string, OutOfMemoryError error)
1755   {
1756     new OOMWarning(string, error, this);
1757   }
1758
1759   @Override
1760   public jalview.api.FeatureRenderer cloneFeatureRenderer()
1761   {
1762
1763     return new FeatureRenderer(this);
1764   }
1765
1766   @Override
1767   public jalview.api.FeatureRenderer getFeatureRenderer()
1768   {
1769     return seqPanel.seqCanvas.getFeatureRenderer();
1770   }
1771
1772   public void updateFeatureRenderer(
1773           jalview.renderer.seqfeatures.FeatureRenderer fr)
1774   {
1775     fr.transferSettings(getSeqPanel().seqCanvas.getFeatureRenderer());
1776   }
1777
1778   public void updateFeatureRendererFrom(jalview.api.FeatureRenderer fr)
1779   {
1780     if (getSeqPanel().seqCanvas.getFeatureRenderer() != null)
1781     {
1782       getSeqPanel().seqCanvas.getFeatureRenderer().transferSettings(fr);
1783     }
1784   }
1785
1786   public ScalePanel getScalePanel()
1787   {
1788     return scalePanel;
1789   }
1790
1791   public void setScalePanel(ScalePanel scalePanel)
1792   {
1793     this.scalePanel = scalePanel;
1794   }
1795
1796   public SeqPanel getSeqPanel()
1797   {
1798     return seqPanel;
1799   }
1800
1801   public void setSeqPanel(SeqPanel seqPanel)
1802   {
1803     this.seqPanel = seqPanel;
1804   }
1805
1806   public AnnotationPanel getAnnotationPanel()
1807   {
1808     return annotationPanel;
1809   }
1810
1811   public void setAnnotationPanel(AnnotationPanel annotationPanel)
1812   {
1813     this.annotationPanel = annotationPanel;
1814   }
1815
1816   public AnnotationLabels getAlabels()
1817   {
1818     return alabels;
1819   }
1820
1821   public void setAlabels(AnnotationLabels alabels)
1822   {
1823     this.alabels = alabels;
1824   }
1825
1826   public IdPanel getIdPanel()
1827   {
1828     return idPanel;
1829   }
1830
1831   public void setIdPanel(IdPanel idPanel)
1832   {
1833     this.idPanel = idPanel;
1834   }
1835
1836   /**
1837    * Follow a scrolling change in the (cDNA/Protein) complementary alignment.
1838    * The aim is to keep the two alignments 'lined up' on their centre columns.
1839    * 
1840    * @param sr
1841    *          holds mapped region(s) of this alignment that we are scrolling
1842    *          'to'; may be modified for sequence offset by this method
1843    * @param verticalOffset
1844    *          the number of visible sequences to show above the mapped region
1845    */
1846   public void scrollToCentre(SearchResultsI sr, int verticalOffset)
1847   {
1848     /*
1849      * To avoid jumpy vertical scrolling (if some sequences are gapped or not
1850      * mapped), we can make the scroll-to location a sequence above the one
1851      * actually mapped.
1852      */
1853     SequenceI mappedTo = sr.getResults().get(0).getSequence();
1854     List<SequenceI> seqs = av.getAlignment().getSequences();
1855
1856     /*
1857      * This is like AlignmentI.findIndex(seq) but here we are matching the
1858      * dataset sequence not the aligned sequence
1859      */
1860     boolean matched = false;
1861     for (SequenceI seq : seqs)
1862     {
1863       if (mappedTo == seq.getDatasetSequence())
1864       {
1865         matched = true;
1866         break;
1867       }
1868     }
1869     if (!matched)
1870     {
1871       return; // failsafe, shouldn't happen
1872     }
1873
1874     /*
1875      * Scroll to position but centring the target residue.
1876      */
1877     scrollToPosition(sr, verticalOffset, true, true);
1878   }
1879
1880   /**
1881    * Set a flag to say do not scroll any (cDNA/protein) complement.
1882    * 
1883    * @param b
1884    */
1885   protected void setDontScrollComplement(boolean b)
1886   {
1887     this.dontScrollComplement = b;
1888   }
1889
1890   protected boolean isDontScrollComplement()
1891   {
1892     return this.dontScrollComplement;
1893   }
1894
1895   /**
1896    * Redraw sensibly.
1897    * 
1898    * @adjustHeight if true, try to recalculate panel height for visible
1899    *               annotations
1900    */
1901   protected void refresh(boolean adjustHeight)
1902   {
1903     validateAnnotationDimensions(adjustHeight);
1904     addNotify();
1905     if (adjustHeight)
1906     {
1907       // sort, repaint, update overview
1908       paintAlignment(true);
1909     }
1910     else
1911     {
1912       // lightweight repaint
1913       repaint();
1914     }
1915   }
1916 }