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