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