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