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