4e1fcab04fda7ee5f0acfa6a6158fd86795e6fca
[jalview.git] / src / jalview / gui / AlignViewport.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.Container;
24 import java.awt.Dimension;
25 import java.awt.Font;
26 import java.awt.FontMetrics;
27 import java.awt.Rectangle;
28 import java.io.File;
29 import java.util.ArrayList;
30 import java.util.Hashtable;
31 import java.util.List;
32
33 import javax.swing.JInternalFrame;
34
35 import jalview.analysis.AlignmentUtils;
36 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
37 import jalview.api.AlignViewportI;
38 import jalview.api.AlignmentViewPanel;
39 import jalview.api.FeatureColourI;
40 import jalview.api.FeatureSettingsModelI;
41 import jalview.api.FeaturesDisplayedI;
42 import jalview.api.ViewStyleI;
43 import jalview.bin.Cache;
44 import jalview.bin.Console;
45 import jalview.commands.CommandI;
46 import jalview.datamodel.AlignedCodonFrame;
47 import jalview.datamodel.Alignment;
48 import jalview.datamodel.AlignmentI;
49 import jalview.datamodel.ColumnSelection;
50 import jalview.datamodel.ContactMatrixI;
51 import jalview.datamodel.HiddenColumns;
52 import jalview.datamodel.SearchResults;
53 import jalview.datamodel.SearchResultsI;
54 import jalview.datamodel.SequenceGroup;
55 import jalview.datamodel.SequenceI;
56 import jalview.io.AppletFormatAdapter;
57 import jalview.io.DataSourceType;
58 import jalview.io.FileFormatException;
59 import jalview.io.FileFormatI;
60 import jalview.io.FileFormats;
61 import jalview.io.FileLoader;
62 import jalview.io.IdentifyFile;
63 import jalview.renderer.ResidueShader;
64 import jalview.schemes.ColourSchemeI;
65 import jalview.schemes.ColourSchemeProperty;
66 import jalview.schemes.ResidueColourScheme;
67 import jalview.schemes.UserColourScheme;
68 import jalview.structure.SelectionSource;
69 import jalview.structure.StructureSelectionManager;
70 import jalview.structure.VamsasSource;
71 import jalview.util.ColorUtils;
72 import jalview.util.MessageManager;
73 import jalview.viewmodel.AlignmentViewport;
74 import jalview.ws.params.AutoCalcSetting;
75
76 /**
77  * DOCUMENT ME!
78  * 
79  * @author $author$
80  * @version $Revision: 1.141 $
81  */
82 public class AlignViewport extends AlignmentViewport
83         implements SelectionSource
84 {
85   Font font;
86
87   boolean cursorMode = false;
88
89   boolean antiAlias = false;
90
91   private Rectangle explodedGeometry = null;
92
93   private String viewName = null;
94
95   /*
96    * Flag set true on the view that should 'gather' multiple views of the same
97    * sequence set id when a project is reloaded. Set false on all views when
98    * they are 'exploded' into separate windows. Set true on the current view
99    * when 'Gather' is performed, and also on the first tab when the first new
100    * view is created.
101    */
102   private boolean gatherViewsHere = false;
103
104   private AnnotationColumnChooser annotationColumnSelectionState;
105
106   /**
107    * Creates a new AlignViewport object.
108    * 
109    * @param al
110    *          alignment to view
111    */
112   public AlignViewport(AlignmentI al)
113   {
114     super(al);
115     init();
116   }
117
118   /**
119    * Create a new AlignViewport object with a specific sequence set ID
120    * 
121    * @param al
122    * @param seqsetid
123    *          (may be null - but potential for ambiguous constructor exception)
124    */
125   public AlignViewport(AlignmentI al, String seqsetid)
126   {
127     this(al, seqsetid, null);
128   }
129
130   public AlignViewport(AlignmentI al, String seqsetid, String viewid)
131   {
132     super(al);
133     sequenceSetID = seqsetid;
134     viewId = viewid;
135     // TODO remove these once 2.4.VAMSAS release finished
136     if (seqsetid != null)
137     {
138       Console.debug(
139               "Setting viewport's sequence set id : " + sequenceSetID);
140     }
141     if (viewId != null)
142     {
143       Console.debug("Setting viewport's view id : " + viewId);
144     }
145     init();
146
147   }
148
149   /**
150    * Create a new AlignViewport with hidden regions
151    * 
152    * @param al
153    *          AlignmentI
154    * @param hiddenColumns
155    *          ColumnSelection
156    */
157   public AlignViewport(AlignmentI al, HiddenColumns hiddenColumns)
158   {
159     super(al);
160     if (hiddenColumns != null)
161     {
162       al.setHiddenColumns(hiddenColumns);
163     }
164     init();
165   }
166
167   /**
168    * New viewport with hidden columns and an existing sequence set id
169    * 
170    * @param al
171    * @param hiddenColumns
172    * @param seqsetid
173    *          (may be null)
174    */
175   public AlignViewport(AlignmentI al, HiddenColumns hiddenColumns,
176           String seqsetid)
177   {
178     this(al, hiddenColumns, seqsetid, null);
179   }
180
181   /**
182    * New viewport with hidden columns and an existing sequence set id and viewid
183    * 
184    * @param al
185    * @param hiddenColumns
186    * @param seqsetid
187    *          (may be null)
188    * @param viewid
189    *          (may be null)
190    */
191   public AlignViewport(AlignmentI al, HiddenColumns hiddenColumns,
192           String seqsetid, String viewid)
193   {
194     super(al);
195     sequenceSetID = seqsetid;
196     viewId = viewid;
197     // TODO remove these once 2.4.VAMSAS release finished
198     if (seqsetid != null)
199     {
200       Console.debug(
201               "Setting viewport's sequence set id : " + sequenceSetID);
202     }
203     if (viewId != null)
204     {
205       Console.debug("Setting viewport's view id : " + viewId);
206     }
207
208     if (hiddenColumns != null)
209     {
210       al.setHiddenColumns(hiddenColumns);
211     }
212     init();
213   }
214
215   /**
216    * Apply any settings saved in user preferences
217    */
218   private void applyViewProperties()
219   {
220     antiAlias = Cache.getDefault("ANTI_ALIAS", true);
221
222     viewStyle.setShowJVSuffix(Cache.getDefault("SHOW_JVSUFFIX", true));
223     setShowAnnotation(Cache.getDefault("SHOW_ANNOTATIONS", true));
224
225     setRightAlignIds(Cache.getDefault("RIGHT_ALIGN_IDS", false));
226     setCentreColumnLabels(Cache.getDefault("CENTRE_COLUMN_LABELS", false));
227     autoCalculateConsensus = Cache.getDefault("AUTO_CALC_CONSENSUS", true);
228
229     setPadGaps(Cache.getDefault("PAD_GAPS", true));
230     setShowNPFeats(Cache.getDefault("SHOW_NPFEATS_TOOLTIP", true));
231     setShowDBRefs(Cache.getDefault("SHOW_DBREFS_TOOLTIP", true));
232     viewStyle.setSeqNameItalics(Cache.getDefault("ID_ITALICS", true));
233     viewStyle.setWrapAlignment(Cache.getDefault("WRAP_ALIGNMENT", false));
234     viewStyle.setShowUnconserved(
235             Cache.getDefault("SHOW_UNCONSERVED", false));
236     sortByTree = Cache.getDefault("SORT_BY_TREE", false);
237     followSelection = Cache.getDefault("FOLLOW_SELECTIONS", true);
238     sortAnnotationsBy = SequenceAnnotationOrder
239             .valueOf(Cache.getDefault(Preferences.SORT_ANNOTATIONS,
240                     SequenceAnnotationOrder.NONE.name()));
241     showAutocalculatedAbove = Cache
242             .getDefault(Preferences.SHOW_AUTOCALC_ABOVE, false);
243     viewStyle.setScaleProteinAsCdna(
244             Cache.getDefault(Preferences.SCALE_PROTEIN_TO_CDNA, true));
245   }
246
247   void init()
248   {
249     applyViewProperties();
250
251     String fontName = Cache.getDefault("FONT_NAME", "SansSerif");
252     String fontStyle = Cache.getDefault("FONT_STYLE", Font.PLAIN + "");
253     String fontSize = Cache.getDefault("FONT_SIZE", "10");
254
255     int style = 0;
256
257     if (fontStyle.equals("bold"))
258     {
259       style = 1;
260     }
261     else if (fontStyle.equals("italic"))
262     {
263       style = 2;
264     }
265
266     setFont(new Font(fontName, style, Integer.parseInt(fontSize)), true);
267
268     alignment
269             .setGapCharacter(Cache.getDefault("GAP_SYMBOL", "-").charAt(0));
270
271     // We must set conservation and consensus before setting colour,
272     // as Blosum and Clustal require this to be done
273     if (hconsensus == null && !isDataset)
274     {
275       if (!alignment.isNucleotide())
276       {
277         showConservation = Cache.getDefault("SHOW_CONSERVATION", true);
278         showQuality = Cache.getDefault("SHOW_QUALITY", true);
279         showGroupConservation = Cache.getDefault("SHOW_GROUP_CONSERVATION",
280                 false);
281       }
282       showConsensusHistogram = Cache.getDefault("SHOW_CONSENSUS_HISTOGRAM",
283               true);
284       showSSConsensusHistogram = Cache.getDefault("SHOW_SSCONSENSUS_HISTOGRAM",
285               true);
286       showSequenceLogo = Cache.getDefault("SHOW_CONSENSUS_LOGO", true);
287       normaliseSequenceLogo = Cache.getDefault("NORMALISE_CONSENSUS_LOGO",
288               false);
289       showGroupConsensus = Cache.getDefault("SHOW_GROUP_CONSENSUS", false);
290       showConsensus = Cache.getDefault("SHOW_IDENTITY", true);
291       
292       showSSConsensus = Cache.getDefault("SHOW_SS_CONSENSUS", false);
293
294       showOccupancy = Cache.getDefault(Preferences.SHOW_OCCUPANCY, true);
295     }
296     initAutoAnnotation();
297     String colourProperty = alignment.isNucleotide()
298             ? Preferences.DEFAULT_COLOUR_NUC
299             : Preferences.DEFAULT_COLOUR_PROT;
300     String schemeName = Cache.getProperty(colourProperty);
301     if (schemeName == null)
302     {
303       // only DEFAULT_COLOUR available in Jalview before 2.9
304       schemeName = Cache.getDefault(Preferences.DEFAULT_COLOUR,
305               ResidueColourScheme.NONE);
306     }
307     ColourSchemeI colourScheme = ColourSchemeProperty.getColourScheme(this,
308             alignment, schemeName);
309     residueShading = new ResidueShader(colourScheme);
310
311     if (colourScheme instanceof UserColourScheme)
312     {
313       residueShading = new ResidueShader(
314               UserDefinedColours.loadDefaultColours());
315       residueShading.setThreshold(0, isIgnoreGapsConsensus());
316     }
317
318     if (residueShading != null)
319     {
320       residueShading.setConsensus(hconsensus);
321       residueShading.setSsConsensus(hSSConsensus);
322     }
323     setColourAppliesToAllGroups(true);
324   }
325
326   boolean validCharWidth;
327
328   /**
329    * {@inheritDoc}
330    */
331   @Override
332   public void setFont(Font f, boolean setGrid)
333   {
334     font = f;
335
336     Container c = new Container();
337
338     if (setGrid)
339     {
340       FontMetrics fm = c.getFontMetrics(font);
341       int ww = fm.charWidth('M');
342       setCharHeight(fm.getHeight());
343       setCharWidth(ww);
344     }
345     viewStyle.setFontName(font.getName());
346     viewStyle.setFontStyle(font.getStyle());
347     viewStyle.setFontSize(font.getSize());
348
349     validCharWidth = true;
350   }
351
352   @Override
353   public void setViewStyle(ViewStyleI settingsForView)
354   {
355     super.setViewStyle(settingsForView);
356     setFont(new Font(viewStyle.getFontName(), viewStyle.getFontStyle(),
357             viewStyle.getFontSize()), false);
358   }
359
360   /**
361    * DOCUMENT ME!
362    * 
363    * @return DOCUMENT ME!
364    */
365   public Font getFont()
366   {
367     return font;
368   }
369
370   /**
371    * DOCUMENT ME!
372    * 
373    * @param align
374    *          DOCUMENT ME!
375    */
376   @Override
377   public void setAlignment(AlignmentI align)
378   {
379     replaceMappings(align);
380     super.setAlignment(align);
381   }
382
383   /**
384    * Replace any codon mappings for this viewport with those for the given
385    * viewport
386    * 
387    * @param align
388    */
389   public void replaceMappings(AlignmentI align)
390   {
391
392     /*
393      * Deregister current mappings (if any)
394      */
395     deregisterMappings();
396
397     /*
398      * Register new mappings (if any)
399      */
400     if (align != null)
401     {
402       StructureSelectionManager ssm = StructureSelectionManager
403               .getStructureSelectionManager(Desktop.instance);
404       ssm.registerMappings(align.getCodonFrames());
405     }
406
407     /*
408      * replace mappings on our alignment
409      */
410     if (alignment != null && align != null)
411     {
412       alignment.setCodonFrames(align.getCodonFrames());
413     }
414   }
415
416   protected void deregisterMappings()
417   {
418     AlignmentI al = getAlignment();
419     if (al != null)
420     {
421       List<AlignedCodonFrame> mappings = al.getCodonFrames();
422       if (mappings != null)
423       {
424         StructureSelectionManager ssm = StructureSelectionManager
425                 .getStructureSelectionManager(Desktop.instance);
426         for (AlignedCodonFrame acf : mappings)
427         {
428           if (noReferencesTo(acf))
429           {
430             ssm.deregisterMapping(acf);
431           }
432         }
433       }
434     }
435   }
436
437   /**
438    * DOCUMENT ME!
439    * 
440    * @return DOCUMENT ME!
441    */
442   @Override
443   public char getGapCharacter()
444   {
445     return getAlignment().getGapCharacter();
446   }
447
448   /**
449    * DOCUMENT ME!
450    * 
451    * @param gap
452    *          DOCUMENT ME!
453    */
454   public void setGapCharacter(char gap)
455   {
456     if (getAlignment() != null)
457     {
458       getAlignment().setGapCharacter(gap);
459     }
460   }
461
462   /**
463    * get hash of undo and redo list for the alignment
464    * 
465    * @return long[] { historyList.hashCode, redoList.hashCode };
466    */
467   public long[] getUndoRedoHash()
468   {
469     // TODO: JAL-1126
470     if (historyList == null || redoList == null)
471     {
472       return new long[] { -1, -1 };
473     }
474     return new long[] { historyList.hashCode(), this.redoList.hashCode() };
475   }
476
477   /**
478    * test if a particular set of hashcodes are different to the hashcodes for
479    * the undo and redo list.
480    * 
481    * @param undoredo
482    *          the stored set of hashcodes as returned by getUndoRedoHash
483    * @return true if the hashcodes differ (ie the alignment has been edited) or
484    *         the stored hashcode array differs in size
485    */
486   public boolean isUndoRedoHashModified(long[] undoredo)
487   {
488     if (undoredo == null)
489     {
490       return true;
491     }
492     long[] cstate = getUndoRedoHash();
493     if (cstate.length != undoredo.length)
494     {
495       return true;
496     }
497
498     for (int i = 0; i < cstate.length; i++)
499     {
500       if (cstate[i] != undoredo[i])
501       {
502         return true;
503       }
504     }
505     return false;
506   }
507
508   public boolean followSelection = true;
509
510   /**
511    * @return true if view selection should always follow the selections
512    *         broadcast by other selection sources
513    */
514   public boolean getFollowSelection()
515   {
516     return followSelection;
517   }
518
519   /**
520    * Send the current selection to be broadcast to any selection listeners.
521    */
522   @Override
523   public void sendSelection()
524   {
525     jalview.structure.StructureSelectionManager
526             .getStructureSelectionManager(Desktop.instance)
527             .sendSelection(new SequenceGroup(getSelectionGroup()),
528                     new ColumnSelection(getColumnSelection()),
529                     new HiddenColumns(getAlignment().getHiddenColumns()),
530                     this);
531   }
532
533   /**
534    * return the alignPanel containing the given viewport. Use this to get the
535    * components currently handling the given viewport.
536    * 
537    * @param av
538    * @return null or an alignPanel guaranteed to have non-null alignFrame
539    *         reference
540    */
541   public AlignmentPanel getAlignPanel()
542   {
543     AlignmentPanel[] aps = PaintRefresher
544             .getAssociatedPanels(this.getSequenceSetId());
545     for (int p = 0; aps != null && p < aps.length; p++)
546     {
547       if (aps[p].av == this)
548       {
549         return aps[p];
550       }
551     }
552     return null;
553   }
554
555   public boolean getSortByTree()
556   {
557     return sortByTree;
558   }
559
560   public void setSortByTree(boolean sort)
561   {
562     sortByTree = sort;
563   }
564
565   /**
566    * Returns the (Desktop) instance of the StructureSelectionManager
567    */
568   @Override
569   public StructureSelectionManager getStructureSelectionManager()
570   {
571     return StructureSelectionManager
572             .getStructureSelectionManager(Desktop.instance);
573   }
574
575   @Override
576   public boolean isNormaliseSequenceLogo()
577   {
578     return normaliseSequenceLogo;
579   }
580
581   public void setNormaliseSequenceLogo(boolean state)
582   {
583     normaliseSequenceLogo = state;
584   }
585
586   /**
587    * 
588    * @return true if alignment characters should be displayed
589    */
590   @Override
591   public boolean isValidCharWidth()
592   {
593     return validCharWidth;
594   }
595
596   private Hashtable<String, AutoCalcSetting> calcIdParams = new Hashtable<>();
597
598   public AutoCalcSetting getCalcIdSettingsFor(String calcId)
599   {
600     return calcIdParams.get(calcId);
601   }
602
603   public void setCalcIdSettingsFor(String calcId, AutoCalcSetting settings,
604           boolean needsUpdate)
605   {
606     calcIdParams.put(calcId, settings);
607     // TODO: create a restart list to trigger any calculations that need to be
608     // restarted after load
609     // calculator.getRegisteredWorkersOfClass(settings.getWorkerClass())
610     if (needsUpdate)
611     {
612       Console.debug("trigger update for " + calcId);
613     }
614   }
615
616   /**
617    * Method called when another alignment's edit (or possibly other) command is
618    * broadcast to here.
619    *
620    * To allow for sequence mappings (e.g. protein to cDNA), we have to first
621    * 'unwind' the command on the source sequences (in simulation, not in fact),
622    * and then for each edit in turn:
623    * <ul>
624    * <li>compute the equivalent edit on the mapped sequences</li>
625    * <li>apply the mapped edit</li>
626    * <li>'apply' the source edit to the working copy of the source
627    * sequences</li>
628    * </ul>
629    * 
630    * @param command
631    * @param undo
632    * @param ssm
633    */
634   @Override
635   public void mirrorCommand(CommandI command, boolean undo,
636           StructureSelectionManager ssm, VamsasSource source)
637   {
638     /*
639      * Do nothing unless we are a 'complement' of the source. May replace this
640      * with direct calls not via SSM.
641      */
642     if (source instanceof AlignViewportI
643             && ((AlignViewportI) source).getCodingComplement() == this)
644     {
645       // ok to continue;
646     }
647     else
648     {
649       return;
650     }
651
652     CommandI mappedCommand = ssm.mapCommand(command, undo, getAlignment(),
653             getGapCharacter());
654     if (mappedCommand != null)
655     {
656       AlignmentI[] views = getAlignPanel().alignFrame.getViewAlignments();
657       mappedCommand.doCommand(views);
658       getAlignPanel().alignmentChanged();
659     }
660   }
661
662   /**
663    * Add the sequences from the given alignment to this viewport. Optionally,
664    * may give the user the option to open a new frame, or split panel, with cDNA
665    * and protein linked.
666    * 
667    * @param toAdd
668    * @param title
669    */
670   public void addAlignment(AlignmentI toAdd, String title)
671   {
672     // TODO: promote to AlignViewportI? applet CutAndPasteTransfer is different
673
674     // JBPComment: title is a largely redundant parameter at the moment
675     // JBPComment: this really should be an 'insert/pre/append' controller
676     // JBPComment: but the DNA/Protein check makes it a bit more complex
677
678     // refactored from FileLoader / CutAndPasteTransfer / SequenceFetcher with
679     // this comment:
680     // TODO: create undo object for this JAL-1101
681
682     /*
683      * Ensure datasets are created for the new alignment as
684      * mappings operate on dataset sequences
685      */
686     toAdd.setDataset(null);
687
688     /*
689      * Check if any added sequence could be the object of a mapping or
690      * cross-reference; if so, make the mapping explicit 
691      */
692     getAlignment().realiseMappings(toAdd.getSequences());
693
694     /*
695      * If any cDNA/protein mappings exist or can be made between the alignments, 
696      * offer to open a split frame with linked alignments
697      */
698     if (Cache.getDefault(Preferences.ENABLE_SPLIT_FRAME, true))
699     {
700       if (AlignmentUtils.isMappable(toAdd, getAlignment()))
701       {
702         openLinkedAlignment(toAdd, title);
703         return;
704       }
705     }
706     addDataToAlignment(toAdd);
707   }
708
709   /**
710    * adds sequences to this alignment
711    * 
712    * @param toAdd
713    */
714   void addDataToAlignment(AlignmentI toAdd)
715   {
716     // TODO: JAL-407 regardless of above - identical sequences (based on ID and
717     // provenance) should share the same dataset sequence
718
719     AlignmentI al = getAlignment();
720     String gap = String.valueOf(al.getGapCharacter());
721     for (int i = 0; i < toAdd.getHeight(); i++)
722     {
723       SequenceI seq = toAdd.getSequenceAt(i);
724       /*
725        * experimental!
726        * - 'align' any mapped sequences as per existing 
727        *    e.g. cdna to genome, domain hit to protein sequence
728        * very experimental! (need a separate menu option for this)
729        * - only add mapped sequences ('select targets from a dataset')
730        */
731       if (true /*AlignmentUtils.alignSequenceAs(seq, al, gap, true, true)*/)
732       {
733         al.addSequence(seq);
734       }
735     }
736     for (ContactMatrixI cm : toAdd.getContactMaps())
737     {
738       al.addContactList(cm);
739     }
740     ranges.setEndSeq(getAlignment().getHeight() - 1); // BH 2019.04.18
741     firePropertyChange("alignment", null, getAlignment().getSequences());
742   }
743
744   /**
745    * Load a File into this AlignViewport attempting to detect format if not
746    * given or given as null.
747    * 
748    * @param file
749    * @param format
750    */
751   public void addFile(File file, FileFormatI format)
752   {
753     addFile(file, format, true);
754   }
755
756   public void addFile(File file, FileFormatI format, boolean async)
757   {
758     DataSourceType protocol = AppletFormatAdapter.checkProtocol(file);
759
760     if (format == null)
761     {
762       try
763       {
764         format = new IdentifyFile().identify(file, protocol);
765       } catch (FileFormatException e1)
766       {
767         jalview.bin.Console.error("Unknown file format for '" + file + "'");
768       }
769     }
770     else if (FileFormats.getInstance().isIdentifiable(format))
771     {
772       try
773       {
774         format = new IdentifyFile().identify(file, protocol);
775       } catch (FileFormatException e)
776       {
777         jalview.bin.Console.error("Unknown file format for '" + file + "'",
778                 e);
779       }
780     }
781
782     new FileLoader().LoadFile(this, file, DataSourceType.FILE, format,
783             async);
784   }
785
786   public void addFile(File file)
787   {
788     addFile(file, null);
789   }
790
791   /**
792    * Show a dialog with the option to open and link (cDNA <-> protein) as a new
793    * alignment, either as a standalone alignment or in a split frame. Returns
794    * true if the new alignment was opened, false if not, because the user
795    * declined the offer.
796    * 
797    * @param al
798    * @param title
799    */
800   protected void openLinkedAlignment(AlignmentI al, String title)
801   {
802     String[] options = new String[] { MessageManager.getString("action.no"),
803         MessageManager.getString("label.split_window"),
804         MessageManager.getString("label.new_window"), };
805     final String question = JvSwingUtils.wrapTooltip(true,
806             MessageManager.getString("label.open_split_window?"));
807     final AlignViewport us = this;
808
809     /*
810      * options No, Split Window, New Window correspond to
811      * dialog responses 0, 1, 2 (even though JOptionPane shows them
812      * in reverse order)
813      */
814     JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.desktop)
815             .setResponseHandler(0, () -> {
816               addDataToAlignment(al);
817             }).setResponseHandler(1, () -> {
818               us.openLinkedAlignmentAs(al, title, true);
819             }).setResponseHandler(2, () -> {
820               us.openLinkedAlignmentAs(al, title, false);
821             });
822     dialog.showDialog(question,
823             MessageManager.getString("label.open_split_window"),
824             JvOptionPane.DEFAULT_OPTION, JvOptionPane.PLAIN_MESSAGE, null,
825             options, options[0]);
826   }
827
828   protected void openLinkedAlignmentAs(AlignmentI al, String title,
829           boolean newWindowOrSplitPane)
830   {
831     /*
832      * Identify protein and dna alignments. Make a copy of this one if opening
833      * in a new split pane.
834      */
835     AlignmentI thisAlignment = newWindowOrSplitPane
836             ? new Alignment(getAlignment())
837             : getAlignment();
838     AlignmentI protein = al.isNucleotide() ? thisAlignment : al;
839     final AlignmentI cdna = al.isNucleotide() ? al : thisAlignment;
840
841     /*
842      * Map sequences. At least one should get mapped as we have already passed
843      * the test for 'mappability'. Any mappings made will be added to the
844      * protein alignment. Note creating dataset sequences on the new alignment
845      * is a pre-requisite for building mappings.
846      */
847     al.setDataset(null);
848     AlignmentUtils.mapProteinAlignmentToCdna(protein, cdna);
849
850     /*
851      * Create the AlignFrame for the added alignment. If it is protein, mappings
852      * are registered with StructureSelectionManager as a side-effect.
853      */
854     AlignFrame newAlignFrame = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
855             AlignFrame.DEFAULT_HEIGHT);
856     newAlignFrame.setTitle(title);
857     newAlignFrame.setStatus(MessageManager
858             .formatMessage("label.successfully_loaded_file", new Object[]
859             { title }));
860
861     // TODO if we want this (e.g. to enable reload of the alignment from file),
862     // we will need to add parameters to the stack.
863     // if (!protocol.equals(DataSourceType.PASTE))
864     // {
865     // alignFrame.setFileName(file, format);
866     // }
867
868     if (!newWindowOrSplitPane)
869     {
870       Desktop.addInternalFrame(newAlignFrame, title,
871               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
872     }
873
874     try
875     {
876       newAlignFrame.setMaximum(Cache.getDefault("SHOW_FULLSCREEN", false));
877     } catch (java.beans.PropertyVetoException ex)
878     {
879     }
880
881     if (newWindowOrSplitPane)
882     {
883       al.alignAs(thisAlignment);
884       protein = openSplitFrame(newAlignFrame, thisAlignment);
885     }
886   }
887
888   /**
889    * Helper method to open a new SplitFrame holding linked dna and protein
890    * alignments.
891    * 
892    * @param newAlignFrame
893    *          containing a new alignment to be shown
894    * @param complement
895    *          cdna/protein complement alignment to show in the other split half
896    * @return the protein alignment in the split frame
897    */
898   protected AlignmentI openSplitFrame(AlignFrame newAlignFrame,
899           AlignmentI complement)
900   {
901     /*
902      * Make a new frame with a copy of the alignment we are adding to. If this
903      * is protein, the mappings to cDNA will be registered with
904      * StructureSelectionManager as a side-effect.
905      */
906     AlignFrame copyMe = new AlignFrame(complement, AlignFrame.DEFAULT_WIDTH,
907             AlignFrame.DEFAULT_HEIGHT);
908     copyMe.setTitle(getAlignPanel().alignFrame.getTitle());
909
910     AlignmentI al = newAlignFrame.viewport.getAlignment();
911     final AlignFrame proteinFrame = al.isNucleotide() ? copyMe
912             : newAlignFrame;
913     final AlignFrame cdnaFrame = al.isNucleotide() ? newAlignFrame : copyMe;
914     cdnaFrame.setVisible(true);
915     proteinFrame.setVisible(true);
916     String linkedTitle = MessageManager
917             .getString("label.linked_view_title");
918
919     /*
920      * Open in split pane. DNA sequence above, protein below.
921      */
922     JInternalFrame splitFrame = new SplitFrame(cdnaFrame, proteinFrame);
923     Desktop.addInternalFrame(splitFrame, linkedTitle, -1, -1);
924
925     return proteinFrame.viewport.getAlignment();
926   }
927
928   public AnnotationColumnChooser getAnnotationColumnSelectionState()
929   {
930     return annotationColumnSelectionState;
931   }
932
933   public void setAnnotationColumnSelectionState(
934           AnnotationColumnChooser currentAnnotationColumnSelectionState)
935   {
936     this.annotationColumnSelectionState = currentAnnotationColumnSelectionState;
937   }
938
939   @Override
940   public void setIdWidth(int i)
941   {
942     super.setIdWidth(i);
943     AlignmentPanel ap = getAlignPanel();
944     if (ap != null)
945     {
946       // modify GUI elements to reflect geometry change
947       Dimension idw = ap.getIdPanel().getIdCanvas().getPreferredSize();
948       idw.width = i;
949       ap.getIdPanel().getIdCanvas().setPreferredSize(idw);
950     }
951   }
952
953   public Rectangle getExplodedGeometry()
954   {
955     return explodedGeometry;
956   }
957
958   public void setExplodedGeometry(Rectangle explodedPosition)
959   {
960     this.explodedGeometry = explodedPosition;
961   }
962
963   public boolean isGatherViewsHere()
964   {
965     return gatherViewsHere;
966   }
967
968   public void setGatherViewsHere(boolean gatherViewsHere)
969   {
970     this.gatherViewsHere = gatherViewsHere;
971   }
972
973   /**
974    * If this viewport has a (Protein/cDNA) complement, then scroll the
975    * complementary alignment to match this one.
976    */
977   public void scrollComplementaryAlignment()
978   {
979     /*
980      * Populate a SearchResults object with the mapped location to scroll to. If
981      * there is no complement, or it is not following highlights, or no mapping
982      * is found, the result will be empty.
983      */
984     SearchResultsI sr = new SearchResults();
985     int verticalOffset = findComplementScrollTarget(sr);
986     if (!sr.isEmpty())
987     {
988       // TODO would like next line without cast but needs more refactoring...
989       final AlignmentPanel complementPanel = ((AlignViewport) getCodingComplement())
990               .getAlignPanel();
991       complementPanel.setToScrollComplementPanel(false);
992       complementPanel.scrollToCentre(sr, verticalOffset);
993       complementPanel.setToScrollComplementPanel(true);
994     }
995   }
996
997   /**
998    * Answers true if no alignment holds a reference to the given mapping
999    * 
1000    * @param acf
1001    * @return
1002    */
1003   protected boolean noReferencesTo(AlignedCodonFrame acf)
1004   {
1005     AlignFrame[] frames = Desktop.getDesktopAlignFrames();
1006     if (frames == null)
1007     {
1008       return true;
1009     }
1010     for (AlignFrame af : frames)
1011     {
1012       if (!af.isClosed())
1013       {
1014         for (AlignmentViewPanel ap : af.getAlignPanels())
1015         {
1016           AlignmentI al = ap.getAlignment();
1017           if (al != null && al.getCodonFrames().contains(acf))
1018           {
1019             return false;
1020           }
1021         }
1022       }
1023     }
1024     return true;
1025   }
1026
1027   /**
1028    * Applies the supplied feature settings descriptor to currently known
1029    * features. This supports an 'initial configuration' of feature colouring
1030    * based on a preset or user favourite. This may then be modified in the usual
1031    * way using the Feature Settings dialogue.
1032    * 
1033    * @param featureSettings
1034    */
1035   @Override
1036   public void applyFeaturesStyle(FeatureSettingsModelI featureSettings)
1037   {
1038     transferFeaturesStyles(featureSettings, false);
1039   }
1040
1041   /**
1042    * Applies the supplied feature settings descriptor to currently known
1043    * features. This supports an 'initial configuration' of feature colouring
1044    * based on a preset or user favourite. This may then be modified in the usual
1045    * way using the Feature Settings dialogue.
1046    * 
1047    * @param featureSettings
1048    */
1049   @Override
1050   public void mergeFeaturesStyle(FeatureSettingsModelI featureSettings)
1051   {
1052     transferFeaturesStyles(featureSettings, true);
1053   }
1054
1055   /**
1056    * when mergeOnly is set, then group and feature visibility or feature colours
1057    * are not modified for features and groups already known to the feature
1058    * renderer. Feature ordering is always adjusted, and transparency is always
1059    * set regardless.
1060    * 
1061    * @param featureSettings
1062    * @param mergeOnly
1063    */
1064   private void transferFeaturesStyles(FeatureSettingsModelI featureSettings,
1065           boolean mergeOnly)
1066   {
1067     if (featureSettings == null)
1068     {
1069       return;
1070     }
1071
1072     FeatureRenderer fr = getAlignPanel().getSeqPanel().seqCanvas
1073             .getFeatureRenderer();
1074     List<String> origRenderOrder = new ArrayList<>();
1075     List<String> origGroups = new ArrayList<>();
1076     // preserve original render order - allows differentiation between user
1077     // configured colours and autogenerated ones
1078     origRenderOrder.addAll(fr.getRenderOrder());
1079     origGroups.addAll(fr.getFeatureGroups());
1080
1081     fr.findAllFeatures(true);
1082     List<String> renderOrder = fr.getRenderOrder();
1083     FeaturesDisplayedI displayed = fr.getFeaturesDisplayed();
1084     if (!mergeOnly)
1085     {
1086       // only clear displayed features if we are mergeing
1087       // displayed.clear();
1088     }
1089     // TODO this clears displayed.featuresRegistered - do we care?
1090     //
1091     // JAL-3330 - JBP - yes we do - calling applyFeatureStyle to a view where
1092     // feature visibility has already been configured is not very friendly
1093     /*
1094      * set feature colour if specified by feature settings
1095      * set visibility of all features
1096      */
1097     for (String type : renderOrder)
1098     {
1099       FeatureColourI preferredColour = featureSettings
1100               .getFeatureColour(type);
1101       FeatureColourI origColour = fr.getFeatureStyle(type);
1102       if (!mergeOnly || (!origRenderOrder.contains(type)
1103               || origColour == null
1104               || (!origColour.isGraduatedColour()
1105                       && origColour.getColour() != null
1106                       && origColour.getColour().equals(
1107                               ColorUtils.createColourFromName(type)))))
1108       {
1109         // if we are merging, only update if there wasn't already a colour
1110         // defined for
1111         // this type
1112         if (preferredColour != null)
1113         {
1114           fr.setColour(type, preferredColour);
1115         }
1116         if (featureSettings.isFeatureDisplayed(type))
1117         {
1118           displayed.setVisible(type);
1119         }
1120         else if (featureSettings.isFeatureHidden(type))
1121         {
1122           displayed.setHidden(type);
1123         }
1124       }
1125     }
1126
1127     /*
1128      * set visibility of feature groups
1129      */
1130     for (String group : fr.getFeatureGroups())
1131     {
1132       if (!mergeOnly || !origGroups.contains(group))
1133       {
1134         // when merging, display groups only if the aren't already marked as not
1135         // visible
1136         fr.setGroupVisibility(group,
1137                 featureSettings.isGroupDisplayed(group));
1138       }
1139     }
1140
1141     /*
1142      * order the features
1143      */
1144     if (featureSettings.optimiseOrder())
1145     {
1146       // TODO not supported (yet?)
1147     }
1148     else
1149     {
1150       fr.orderFeatures(featureSettings);
1151     }
1152     fr.setTransparency(featureSettings.getTransparency());
1153
1154     fr.notifyFeaturesChanged();
1155   }
1156
1157   public String getViewName()
1158   {
1159     return viewName;
1160   }
1161
1162   public void setViewName(String viewName)
1163   {
1164     this.viewName = viewName;
1165   }
1166
1167 }