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