Merge branch 'develop' into feature/JAL-3390hideUnmappedStructure
[jalview.git] / src / jalview / gui / StructureViewerBase.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.Color;
24 import java.awt.Component;
25 import java.awt.event.ActionEvent;
26 import java.awt.event.ActionListener;
27 import java.awt.event.ItemEvent;
28 import java.awt.event.ItemListener;
29 import java.io.BufferedReader;
30 import java.io.File;
31 import java.io.FileOutputStream;
32 import java.io.FileReader;
33 import java.io.IOException;
34 import java.io.PrintWriter;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
38 import java.util.Random;
39 import java.util.Vector;
40
41 import javax.swing.ButtonGroup;
42 import javax.swing.JCheckBoxMenuItem;
43 import javax.swing.JMenu;
44 import javax.swing.JMenuItem;
45 import javax.swing.JRadioButtonMenuItem;
46 import javax.swing.event.MenuEvent;
47 import javax.swing.event.MenuListener;
48
49 import jalview.api.AlignmentViewPanel;
50 import jalview.bin.Cache;
51 import jalview.datamodel.AlignmentI;
52 import jalview.datamodel.PDBEntry;
53 import jalview.datamodel.SequenceI;
54 import jalview.gui.JalviewColourChooser.ColourChooserListener;
55 import jalview.gui.StructureViewer.ViewerType;
56 import jalview.gui.ViewSelectionMenu.ViewSetProvider;
57 import jalview.io.DataSourceType;
58 import jalview.io.JalviewFileChooser;
59 import jalview.io.JalviewFileView;
60 import jalview.jbgui.GStructureViewer;
61 import jalview.schemes.ColourSchemeI;
62 import jalview.schemes.ColourSchemes;
63 import jalview.structure.StructureMapping;
64 import jalview.structures.models.AAStructureBindingModel;
65 import jalview.util.BrowserLauncher;
66 import jalview.util.MessageManager;
67 import jalview.ws.dbsources.Pdb;
68
69 /**
70  * Base class with common functionality for JMol, Chimera or other structure
71  * viewers.
72  * 
73  * @author gmcarstairs
74  *
75  */
76 public abstract class StructureViewerBase extends GStructureViewer
77         implements Runnable, ViewSetProvider
78 {
79   private static final String UNMAPPED = "(unmapped)";
80
81   /*
82    * names for colour options (additional to Jalview colour schemes)
83    */
84   enum ViewerColour
85   {
86     BySequence, ByChain, ChargeCysteine, ByViewer
87   }
88
89   /**
90    * list of sequenceSet ids associated with the view
91    */
92   protected List<String> _aps = new ArrayList<>();
93
94   /**
95    * list of alignment panels to use for superposition
96    */
97   protected Vector<AlignmentViewPanel> _alignwith = new Vector<>();
98
99   /**
100    * list of alignment panels that are used for colouring structures by aligned
101    * sequences
102    */
103   protected Vector<AlignmentViewPanel> _colourwith = new Vector<>();
104
105   private String viewId = null;
106
107   private AlignmentPanel ap;
108
109   protected boolean alignAddedStructures = false;
110
111   protected volatile boolean _started = false;
112
113   protected volatile boolean addingStructures = false;
114
115   protected Thread worker = null;
116
117   protected boolean allChainsSelected = false;
118
119   protected JMenu viewSelectionMenu;
120
121   /**
122    * set after sequence colouring has been applied for this structure viewer.
123    * used to determine if the final sequence/structure mapping has been
124    * determined
125    */
126   protected volatile boolean seqColoursApplied = false;
127
128   private IProgressIndicator progressBar = null;
129
130   private Random random = new Random();
131
132   /**
133    * Default constructor
134    */
135   public StructureViewerBase()
136   {
137     super();
138   }
139
140   /**
141    * @return true if added structures should be aligned to existing one(s)
142    */
143   @Override
144   public boolean isAlignAddedStructures()
145   {
146     return alignAddedStructures;
147   }
148
149   /**
150    * 
151    * @param true
152    *          if added structures should be aligned to existing one(s)
153    */
154   @Override
155   public void setAlignAddedStructures(boolean alignAdded)
156   {
157     alignAddedStructures = alignAdded;
158   }
159
160   /**
161    * 
162    * @param ap2
163    * @return true if this Jmol instance is linked with the given alignPanel
164    */
165   public boolean isLinkedWith(AlignmentPanel ap2)
166   {
167     return _aps.contains(ap2.av.getSequenceSetId());
168   }
169
170   public boolean isUsedforaligment(AlignmentViewPanel ap2)
171   {
172
173     return (_alignwith != null) && _alignwith.contains(ap2);
174   }
175
176   @Override
177   public boolean isUsedForColourBy(AlignmentViewPanel avp)
178   {
179     return (_colourwith != null) && _colourwith.contains(avp);
180   }
181
182   /**
183    * 
184    * @return TRUE if the view is NOT being coloured by the alignment colours.
185    */
186   public boolean isColouredByViewer()
187   {
188     return !getBinding().isColourBySequence();
189   }
190
191   public String getViewId()
192   {
193     if (viewId == null)
194     {
195       viewId = System.currentTimeMillis() + "." + this.hashCode();
196     }
197     return viewId;
198   }
199
200   protected void setViewId(String viewId)
201   {
202     this.viewId = viewId;
203   }
204
205   protected void buildActionMenu()
206   {
207     if (_alignwith == null)
208     {
209       _alignwith = new Vector<>();
210     }
211     if (_alignwith.size() == 0 && ap != null)
212     {
213       _alignwith.add(ap);
214     }
215     ;
216     for (Component c : viewerActionMenu.getMenuComponents())
217     {
218       if (c != alignStructs)
219       {
220         viewerActionMenu.remove((JMenuItem) c);
221       }
222     }
223   }
224
225   @Override
226   public AlignmentPanel getAlignmentPanel()
227   {
228     return ap;
229   }
230
231   protected void setAlignmentPanel(AlignmentPanel alp)
232   {
233     this.ap = alp;
234   }
235
236   @Override
237   public AlignmentPanel[] getAllAlignmentPanels()
238   {
239     AlignmentPanel[] t, list = new AlignmentPanel[0];
240     for (String setid : _aps)
241     {
242       AlignmentPanel[] panels = PaintRefresher.getAssociatedPanels(setid);
243       if (panels != null)
244       {
245         t = new AlignmentPanel[list.length + panels.length];
246         System.arraycopy(list, 0, t, 0, list.length);
247         System.arraycopy(panels, 0, t, list.length, panels.length);
248         list = t;
249       }
250     }
251
252     return list;
253   }
254
255   /**
256    * set the primary alignmentPanel reference and add another alignPanel to the
257    * list of ones to use for colouring and aligning
258    * 
259    * @param nap
260    */
261   public void addAlignmentPanel(AlignmentPanel nap)
262   {
263     if (getAlignmentPanel() == null)
264     {
265       setAlignmentPanel(nap);
266     }
267     if (!_aps.contains(nap.av.getSequenceSetId()))
268     {
269       _aps.add(nap.av.getSequenceSetId());
270     }
271   }
272
273   /**
274    * remove any references held to the given alignment panel
275    * 
276    * @param nap
277    */
278   @Override
279   public void removeAlignmentPanel(AlignmentViewPanel nap)
280   {
281     try
282     {
283       _alignwith.remove(nap);
284       _colourwith.remove(nap);
285       if (getAlignmentPanel() == nap)
286       {
287         setAlignmentPanel(null);
288         for (AlignmentPanel aps : getAllAlignmentPanels())
289         {
290           if (aps != nap)
291           {
292             setAlignmentPanel(aps);
293             break;
294           }
295         }
296       }
297     } catch (Exception ex)
298     {
299     }
300     if (getAlignmentPanel() != null)
301     {
302       buildActionMenu();
303     }
304   }
305
306   public void useAlignmentPanelForSuperposition(AlignmentPanel nap)
307   {
308     addAlignmentPanel(nap);
309     if (!_alignwith.contains(nap))
310     {
311       _alignwith.add(nap);
312     }
313   }
314
315   public void excludeAlignmentPanelForSuperposition(AlignmentPanel nap)
316   {
317     if (_alignwith.contains(nap))
318     {
319       _alignwith.remove(nap);
320     }
321   }
322
323   public void useAlignmentPanelForColourbyseq(AlignmentPanel nap,
324           boolean enableColourBySeq)
325   {
326     useAlignmentPanelForColourbyseq(nap);
327     getBinding().setColourBySequence(enableColourBySeq);
328     seqColour.setSelected(enableColourBySeq);
329     viewerColour.setSelected(!enableColourBySeq);
330   }
331
332   public void useAlignmentPanelForColourbyseq(AlignmentPanel nap)
333   {
334     addAlignmentPanel(nap);
335     if (!_colourwith.contains(nap))
336     {
337       _colourwith.add(nap);
338     }
339   }
340
341   public void excludeAlignmentPanelForColourbyseq(AlignmentPanel nap)
342   {
343     if (_colourwith.contains(nap))
344     {
345       _colourwith.remove(nap);
346     }
347   }
348
349   public abstract ViewerType getViewerType();
350
351   /**
352    * add a new structure (with associated sequences and chains) to this viewer,
353    * retrieving it if necessary first.
354    * 
355    * @param pdbentry
356    * @param seqs
357    * @param chains
358    * @param align
359    *          if true, new structure(s) will be aligned using associated
360    *          alignment
361    * @param alignFrame
362    */
363   protected void addStructure(final PDBEntry pdbentry,
364           final SequenceI[] seqs, final String[] chains,
365           final IProgressIndicator alignFrame)
366   {
367     if (pdbentry.getFile() == null)
368     {
369       if (worker != null && worker.isAlive())
370       {
371         // a retrieval is in progress, wait around and add ourselves to the
372         // queue.
373         new Thread(new Runnable()
374         {
375           @Override
376           public void run()
377           {
378             while (worker != null && worker.isAlive() && _started)
379             {
380               try
381               {
382                 Thread.sleep(100 + ((int) Math.random() * 100));
383
384               } catch (Exception e)
385               {
386               }
387             }
388             // and call ourselves again.
389             addStructure(pdbentry, seqs, chains, alignFrame);
390           }
391         }).start();
392         return;
393       }
394     }
395     // otherwise, start adding the structure.
396     getBinding().addSequenceAndChain(new PDBEntry[] { pdbentry },
397             new SequenceI[][]
398             { seqs }, new String[][] { chains });
399     addingStructures = true;
400     _started = false;
401     worker = new Thread(this);
402     worker.start();
403     return;
404   }
405
406   protected boolean hasPdbId(String pdbId)
407   {
408     return getBinding().hasPdbId(pdbId);
409   }
410
411   /**
412    * Returns a list of any viewer of the instantiated type. The list is
413    * restricted to those linked to the given alignment panel if it is not null.
414    */
415   protected List<StructureViewerBase> getViewersFor(AlignmentPanel alp)
416   {
417     return Desktop.instance.getStructureViewers(alp, this.getClass());
418   }
419
420   @Override
421   public void addToExistingViewer(PDBEntry pdbentry, SequenceI[] seq,
422           String[] chains, final AlignmentViewPanel apanel, String pdbId)
423   {
424     /*
425      * JAL-1742 exclude view with this structure already mapped (don't offer
426      * to align chain B to chain A of the same structure); code may defend
427      * against this possibility before we reach here
428      */
429     if (hasPdbId(pdbId))
430     {
431       return;
432     }
433     AlignmentPanel alignPanel = (AlignmentPanel) apanel; // Implementation error if this
434                                                  // cast fails
435     useAlignmentPanelForSuperposition(alignPanel);
436     addStructure(pdbentry, seq, chains, alignPanel.alignFrame);
437   }
438
439   /**
440    * Adds mappings for the given sequences to an already opened PDB structure,
441    * and updates any viewers that have the PDB file
442    * 
443    * @param seq
444    * @param chains
445    * @param apanel
446    * @param pdbFilename
447    */
448   public void addSequenceMappingsToStructure(SequenceI[] seq,
449           String[] chains, final AlignmentViewPanel alpanel,
450           String pdbFilename)
451   {
452     AlignmentPanel apanel = (AlignmentPanel) alpanel;
453
454     // TODO : Fix multiple seq to one chain issue here.
455     /*
456      * create the mappings
457      */
458     apanel.getStructureSelectionManager().setMapping(seq, chains,
459             pdbFilename, DataSourceType.FILE, getProgressIndicator());
460
461     /*
462      * alert the FeatureRenderer to show new (PDB RESNUM) features
463      */
464     if (apanel.getSeqPanel().seqCanvas.fr != null)
465     {
466       apanel.getSeqPanel().seqCanvas.fr.featuresAdded();
467       // note - we don't do a refresh for structure here because we do it
468       // explicitly for all panels later on
469       apanel.paintAlignment(true, false);
470     }
471
472     /*
473      * add the sequences to any other viewers (of the same type) for this pdb
474      * file
475      */
476     // JBPNOTE: this looks like a binding routine, rather than a gui routine
477     for (StructureViewerBase viewer : getViewersFor(null))
478     {
479       AAStructureBindingModel bindingModel = viewer.getBinding();
480       for (int pe = 0; pe < bindingModel.getPdbCount(); pe++)
481       {
482         if (bindingModel.getPdbEntry(pe).getFile().equals(pdbFilename))
483         {
484           bindingModel.addSequence(pe, seq);
485           viewer.addAlignmentPanel(apanel);
486           /*
487            * add it to the set of alignments used for colouring structure by
488            * sequence
489            */
490           viewer.useAlignmentPanelForColourbyseq(apanel);
491           viewer.buildActionMenu();
492           apanel.getStructureSelectionManager()
493                   .sequenceColoursChanged(apanel);
494           break;
495         }
496       }
497     }
498   }
499
500   @Override
501   public boolean addAlreadyLoadedFile(SequenceI[] seq, String[] chains,
502           final AlignmentViewPanel apanel, String pdbId)
503   {
504     String alreadyMapped = apanel.getStructureSelectionManager()
505             .alreadyMappedToFile(pdbId);
506
507     if (alreadyMapped == null)
508     {
509       return false;
510     }
511
512     addSequenceMappingsToStructure(seq, chains, apanel, alreadyMapped);
513     return true;
514   }
515
516   /**
517    * Add menu items to the 'Show Chains' sub-menu, given a list of values formatted as "pdbId:chainCode"
518    * @param chainNames
519    */
520   void setChainMenuItems(List<String> chainNames)
521   {
522     chainMenu.removeAll();
523     if (chainNames == null || chainNames.isEmpty())
524     {
525       return;
526     }
527
528     /*
529      * add the 'All' menu item
530      */
531     JMenuItem menuItem = new JMenuItem(
532             MessageManager.getString("label.all"));
533     menuItem.addActionListener(new ActionListener()
534     {
535       @Override
536       public void actionPerformed(ActionEvent evt)
537       {
538         allChainsSelected = true;
539         for (int i = 0; i < chainMenu.getItemCount(); i++)
540         {
541           if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
542           {
543             ((JCheckBoxMenuItem) chainMenu.getItem(i)).setSelected(true);
544           }
545         }
546         showSelectedChains();
547         allChainsSelected = false;
548       }
549     });
550     chainMenu.add(menuItem);
551
552     /*
553      * add a menu item for each structure and chain
554      */
555     Collections.sort(chainNames);
556     boolean hideUnmapped = getBinding().isShowAlignmentOnly();
557     for (String chain : chainNames)
558     {
559       String[] tokens = chain.split(":");
560       String pdbId = tokens[0];
561       String chainCode = tokens[1];
562       String seqName = getSequenceNameForChain(chain);
563       boolean isUnmapped = seqName == null;
564       if (isUnmapped)
565       {
566         seqName = UNMAPPED;
567       }
568       int nameLength = seqName.length();
569       if (nameLength > 16)
570       {
571         seqName = seqName.substring(0, 8) + "..."
572                 + seqName.substring(nameLength - 8, nameLength);
573       }
574       String text = chain + " " + seqName;
575       menuItem = new JCheckBoxMenuItem(text, true);
576       
577       /*
578        * set the item's state _before_ adding its listener!
579        */
580       menuItem.setSelected(getBinding().isShowChain(pdbId,  chainCode));
581       if (hideUnmapped && isUnmapped)
582       {
583         menuItem.setEnabled(false);
584       }
585       menuItem.addItemListener(new ItemListener()
586       {
587         @Override
588         public void itemStateChanged(ItemEvent evt)
589         {
590           if (!allChainsSelected)
591           {
592             showSelectedChains();
593           }
594         }
595       });
596       chainMenu.add(menuItem);
597     }
598   }
599
600   /**
601    * Answers the name of the sequence mapped to the given chain (formatted as
602    * pdbId:chainId, e.g. 1A70:A). Answers null if no mapped sequence is found. If
603    * more than one sequence is matched, just answers the name of the first one
604    * found.
605    * 
606    * @param chain
607    * @return
608    */
609   private String getSequenceNameForChain(String chain)
610   {
611     String[] tokens = chain.split(":");
612     String pdbId = tokens[0];
613     String chainId = tokens[1];
614     List<StructureMapping> mappings = getBinding().getSsm()
615             .getMappingForChain(pdbId, chainId);
616     return mappings.isEmpty() ? null
617             : mappings.get(0).getSequence().getName();
618   }
619
620   /**
621    * Action on selecting one of Jalview's registered colour schemes
622    */
623   @Override
624   public void changeColour_actionPerformed(String colourSchemeName)
625   {
626     AlignmentI al = getAlignmentPanel().av.getAlignment();
627     ColourSchemeI cs = ColourSchemes.getInstance()
628             .getColourScheme(colourSchemeName, getAlignmentPanel().av, al,
629                     null);
630     getBinding().colourByJalviewColourScheme(cs);
631   }
632
633   /**
634    * Builds the colour menu
635    */
636   protected void buildColourMenu()
637   {
638     colourMenu.removeAll();
639     AlignmentI al = getAlignmentPanel().av.getAlignment();
640
641     /*
642      * add colour by sequence, by chain, by charge and cysteine
643      */
644     colourMenu.add(seqColour);
645     colourMenu.add(chainColour);
646     colourMenu.add(chargeColour);
647     chargeColour.setEnabled(!al.isNucleotide());
648
649     /*
650      * add all 'simple' (per-residue) colour schemes registered to Jalview
651      */
652     ButtonGroup itemGroup = ColourMenuHelper.addMenuItems(colourMenu, this,
653             al, true);
654
655     /*
656      * add 'colour by viewer' (menu item text is set in subclasses)
657      */
658     viewerColour.setSelected(false);
659     viewerColour.addActionListener(new ActionListener()
660     {
661       @Override
662       public void actionPerformed(ActionEvent actionEvent)
663       {
664         viewerColour_actionPerformed();
665       }
666     });
667     colourMenu.add(viewerColour);
668
669     /*
670      * add 'set background colour'
671      */
672     JMenuItem backGround = new JMenuItem();
673     backGround
674             .setText(MessageManager.getString("action.background_colour"));
675     backGround.addActionListener(new ActionListener()
676     {
677       @Override
678       public void actionPerformed(ActionEvent actionEvent)
679       {
680         background_actionPerformed();
681       }
682     });
683     colourMenu.add(backGround);
684
685     /*
686      * add colour buttons to a group so their selection is
687      * mutually exclusive (background colour is a separate option)
688      */
689     itemGroup.add(seqColour);
690     itemGroup.add(chainColour);
691     itemGroup.add(chargeColour);
692     itemGroup.add(viewerColour);
693   }
694
695   /**
696    * Construct menu items
697    */
698   protected void initMenus()
699   {
700     AAStructureBindingModel binding = getBinding();
701
702     seqColour = new JRadioButtonMenuItem();
703     seqColour.setText(MessageManager.getString("action.by_sequence"));
704     seqColour.setName(ViewerColour.BySequence.name());
705     seqColour.setSelected(binding.isColourBySequence());
706     seqColour.addActionListener(new ActionListener()
707     {
708       @Override
709       public void actionPerformed(ActionEvent actionEvent)
710       {
711         seqColour_actionPerformed();
712       }
713     });
714
715     chainColour = new JRadioButtonMenuItem();
716     chainColour.setText(MessageManager.getString("action.by_chain"));
717     chainColour.setName(ViewerColour.ByChain.name());
718     chainColour.addActionListener(new ActionListener()
719     {
720       @Override
721       public void actionPerformed(ActionEvent actionEvent)
722       {
723         chainColour_actionPerformed();
724       }
725     });
726
727     chargeColour = new JRadioButtonMenuItem();
728     chargeColour.setText(MessageManager.getString("label.charge_cysteine"));
729     chargeColour.setName(ViewerColour.ChargeCysteine.name());
730     chargeColour.addActionListener(new ActionListener()
731     {
732       @Override
733       public void actionPerformed(ActionEvent actionEvent)
734       {
735         chargeColour_actionPerformed();
736       }
737     });
738
739     viewerColour = new JRadioButtonMenuItem();
740     viewerColour
741             .setText(MessageManager.getString("label.colour_with_viewer"));
742     viewerColour.setToolTipText(MessageManager
743             .getString("label.let_viewer_manage_structure_colours"));
744     viewerColour.setName(ViewerColour.ByViewer.name());
745     viewerColour.setSelected(!binding.isColourBySequence());
746
747     if (_colourwith == null)
748     {
749       _colourwith = new Vector<>();
750     }
751     if (_alignwith == null)
752     {
753       _alignwith = new Vector<>();
754     }
755
756     ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
757             MessageManager.getString("label.colour_by"), this, _colourwith,
758             new ItemListener()
759             {
760               @Override
761               public void itemStateChanged(ItemEvent e)
762               {
763                 if (!seqColour.isSelected())
764                 {
765                   seqColour.doClick();
766                 }
767                 else
768                 {
769                   // update the viewer display now.
770                   seqColour_actionPerformed();
771                 }
772               }
773             });
774     viewMenu.add(seqColourBy);
775
776     showAlignmentOnly = new JCheckBoxMenuItem(
777             MessageManager.getString("label.show_alignment_only"));
778     showAlignmentOnly.addActionListener(new ActionListener()
779     {
780       @Override
781       public void actionPerformed(ActionEvent e)
782       {
783         hideHiddenRegions.setEnabled(showAlignmentOnly.isSelected());
784         getBinding().setShowAlignmentOnly(showAlignmentOnly.isSelected());
785         getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
786                 true);
787         updateTitleAndMenus(); // to add or remove unmapped chain menu items
788       }
789     });
790     viewMenu.add(showAlignmentOnly);
791
792     hideHiddenRegions = new JCheckBoxMenuItem(
793             MessageManager.getString("label.hide_hidden_regions"));
794     hideHiddenRegions.setEnabled(false);
795     hideHiddenRegions.addActionListener(new ActionListener()
796     {
797       @Override
798       public void actionPerformed(ActionEvent e)
799       {
800         getBinding().setHideHiddenRegions(hideHiddenRegions.isSelected());
801         getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
802                 false);
803       }
804     });
805     viewMenu.add(hideHiddenRegions);
806     viewMenu.add(fitToWindow);
807
808     final ItemListener handler = new ItemListener()
809     {
810       @Override
811       public void itemStateChanged(ItemEvent e)
812       {
813         if (_alignwith.isEmpty())
814         {
815           alignStructs.setEnabled(false);
816           alignStructs.setToolTipText(null);
817         }
818         else
819         {
820           alignStructs.setEnabled(true);
821           alignStructs.setToolTipText(MessageManager.formatMessage(
822                   "label.align_structures_using_linked_alignment_views",
823                   _alignwith.size()));
824         }
825       }
826     };
827     viewSelectionMenu = new ViewSelectionMenu(
828             MessageManager.getString("label.superpose_with"), this,
829             _alignwith, handler);
830     handler.itemStateChanged(null);
831     viewerActionMenu.add(viewSelectionMenu, 0);
832     viewerActionMenu.addMenuListener(new MenuListener()
833     {
834       @Override
835       public void menuSelected(MenuEvent e)
836       {
837         handler.itemStateChanged(null);
838       }
839
840       @Override
841       public void menuDeselected(MenuEvent e)
842       {
843       }
844
845       @Override
846       public void menuCanceled(MenuEvent e)
847       {
848       }
849     });
850
851     viewerActionMenu.setText(getViewerName());
852     helpItem.setText(MessageManager.formatMessage("label.viewer_help",
853             getViewerName()));
854
855     buildColourMenu();
856   }
857
858   /**
859    * Sends commands to the structure viewer to superimpose structures based on
860    * currently associated alignments. May optionally return an error message for
861    * the operation.
862    */
863   @Override
864   protected String alignStructsWithAllAlignPanels()
865   {
866     if (getAlignmentPanel() == null)
867     {
868       return null;
869     }
870
871     if (_alignwith.size() == 0)
872     {
873       _alignwith.add(getAlignmentPanel());
874     }
875
876     String reply = null;
877     try
878     {
879       reply = getBinding().superposeStructures(_alignwith);
880       if (reply != null && !reply.isEmpty())
881       {
882         String text = MessageManager
883                 .formatMessage("error.superposition_failed", reply);
884         statusBar.setText(text);
885       }
886     } catch (Exception e)
887     {
888       StringBuilder sb = new StringBuilder();
889       for (AlignmentViewPanel alignPanel : _alignwith)
890       {
891         sb.append("'").append(alignPanel.getViewName()).append("' ");
892       }
893       Cache.log.info("Couldn't align structures with the " + sb.toString()
894               + "associated alignment panels.", e);
895     }
896     return reply;
897   }
898
899   /**
900    * Opens a colour chooser dialog, and applies the chosen colour to the
901    * background of the structure viewer
902    */
903   @Override
904   public void background_actionPerformed()
905   {
906     String ttl = MessageManager.getString("label.select_background_colour");
907     ColourChooserListener listener = new ColourChooserListener()
908     {
909       @Override
910       public void colourSelected(Color c)
911       {
912         getBinding().setBackgroundColour(c);
913       }
914     };
915     JalviewColourChooser.showColourChooser(this, ttl, null, listener);
916   }
917
918   @Override
919   public void viewerColour_actionPerformed()
920   {
921     if (viewerColour.isSelected())
922     {
923       // disable automatic sequence colouring.
924       getBinding().setColourBySequence(false);
925     }
926   }
927
928   @Override
929   public void chainColour_actionPerformed()
930   {
931     chainColour.setSelected(true);
932     getBinding().colourByChain();
933   }
934
935   @Override
936   public void chargeColour_actionPerformed()
937   {
938     chargeColour.setSelected(true);
939     getBinding().colourByCharge();
940   }
941
942   @Override
943   public void seqColour_actionPerformed()
944   {
945     AAStructureBindingModel binding = getBinding();
946     binding.setColourBySequence(seqColour.isSelected());
947     if (_colourwith == null)
948     {
949       _colourwith = new Vector<>();
950     }
951     if (binding.isColourBySequence())
952     {
953       if (!binding.isLoadingFromArchive())
954       {
955         if (_colourwith.size() == 0 && getAlignmentPanel() != null)
956         {
957           // Make the currently displayed alignment panel the associated view
958           _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
959         }
960       }
961       // Set the colour using the current view for the associated alignframe
962       for (AlignmentViewPanel avp : _colourwith)
963       {
964         binding.updateStructureColours(avp);
965       }
966       seqColoursApplied = true;
967     }
968   }
969
970   @Override
971   public void pdbFile_actionPerformed()
972   {
973     // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
974     JalviewFileChooser chooser = new JalviewFileChooser(
975             Cache.getProperty("LAST_DIRECTORY"));
976
977     chooser.setFileView(new JalviewFileView());
978     chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
979     chooser.setToolTipText(MessageManager.getString("action.save"));
980
981     int value = chooser.showSaveDialog(this);
982
983     if (value == JalviewFileChooser.APPROVE_OPTION)
984     {
985       BufferedReader in = null;
986       try
987       {
988         // TODO: cope with multiple PDB files in view
989         in = new BufferedReader(
990                 new FileReader(getBinding().getStructureFiles()[0]));
991         File outFile = chooser.getSelectedFile();
992
993         PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
994         String data;
995         while ((data = in.readLine()) != null)
996         {
997           if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
998           {
999             out.println(data);
1000           }
1001         }
1002         out.close();
1003       } catch (Exception ex)
1004       {
1005         ex.printStackTrace();
1006       } finally
1007       {
1008         if (in != null)
1009         {
1010           try
1011           {
1012             in.close();
1013           } catch (IOException e)
1014           {
1015             // ignore
1016           }
1017         }
1018       }
1019     }
1020   }
1021
1022   @Override
1023   public void viewMapping_actionPerformed()
1024   {
1025     CutAndPasteTransfer cap = new CutAndPasteTransfer();
1026     try
1027     {
1028       cap.appendText(getBinding().printMappings());
1029     } catch (OutOfMemoryError e)
1030     {
1031       new OOMWarning(
1032               "composing sequence-structure alignments for display in text box.",
1033               e);
1034       cap.dispose();
1035       return;
1036     }
1037     Desktop.addInternalFrame(cap,
1038             MessageManager.getString("label.pdb_sequence_mapping"), 550,
1039             600);
1040   }
1041
1042   protected abstract String getViewerName();
1043
1044   /**
1045    * Configures the title and menu items of the viewer panel.
1046    */
1047   @Override
1048   public void updateTitleAndMenus()
1049   {
1050     AAStructureBindingModel binding = getBinding();
1051     if (binding.hasFileLoadingError())
1052     {
1053       repaint();
1054       return;
1055     }
1056     synchronized (chainMenu)
1057     {
1058       setChainMenuItems(binding.getChainNames());
1059     }
1060
1061     this.setTitle(binding.getViewerTitle(getViewerName(), true));
1062
1063     /*
1064      * enable 'Superpose with' if more than one mapped structure
1065      */
1066     viewSelectionMenu.setEnabled(false);
1067     if (getBinding().getMappedStructureCount() > 1
1068             && getBinding().getSequence().length > 1)
1069     {
1070       viewSelectionMenu.setEnabled(true);
1071     }
1072
1073     /*
1074      * Show action menu if it has any enabled items
1075      */
1076     viewerActionMenu.setVisible(false);
1077     for (int i = 0; i < viewerActionMenu.getItemCount(); i++)
1078     {
1079       if (viewerActionMenu.getItem(i).isEnabled())
1080       {
1081         viewerActionMenu.setVisible(true);
1082         break;
1083       }
1084     }
1085
1086     if (!binding.isLoadingFromArchive())
1087     {
1088       seqColour_actionPerformed();
1089     }
1090   }
1091
1092   @Override
1093   public String toString()
1094   {
1095     return getTitle();
1096   }
1097
1098   @Override
1099   public boolean hasMapping()
1100   {
1101     if (worker != null && (addingStructures || _started))
1102     {
1103       return false;
1104     }
1105     if (getBinding() == null)
1106     {
1107       if (_aps == null || _aps.size() == 0)
1108       {
1109         // viewer has been closed, but we did at some point run.
1110         return true;
1111       }
1112       return false;
1113     }
1114     String[] pdbids = getBinding().getStructureFiles();
1115     if (pdbids == null)
1116     {
1117       return false;
1118     }
1119     int p=0;
1120     for (String pdbid:pdbids) {
1121       StructureMapping sm[] = getBinding().getSsm().getMapping(pdbid);
1122       if (sm!=null && sm.length>0 && sm[0]!=null) {
1123         p++;
1124       }
1125     }
1126     // only return true if there is a mapping for every structure file we have loaded
1127     if (p == 0 || p != pdbids.length)
1128     {
1129       return false;
1130     }
1131     // and that coloring has been applied
1132     return seqColoursApplied;
1133   }
1134
1135   @Override
1136   public void raiseViewer()
1137   {
1138     toFront();
1139   }
1140
1141   @Override
1142   public abstract AAStructureBindingModel getBinding();
1143
1144   /**
1145    * Show only the selected chain(s) in the viewer
1146    */
1147   protected void showSelectedChains()
1148   {
1149     setChainsToHide();
1150   
1151     /*
1152      * refresh display without resizing - easier to see what changed
1153      */
1154     getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
1155             false);
1156   }
1157
1158   @Override
1159   public long startProgressBar(String msg)
1160   {
1161     // TODO would rather have startProgress/stopProgress as the
1162     // IProgressIndicator interface
1163     long tm = random.nextLong();
1164     if (progressBar != null)
1165     {
1166       progressBar.setProgressBar(msg, tm);
1167     }
1168     return tm;
1169   }
1170
1171   @Override
1172   public void stopProgressBar(String msg, long handle)
1173   {
1174     if (progressBar != null)
1175     {
1176       progressBar.setProgressBar(msg, handle);
1177     }
1178   }
1179
1180   protected IProgressIndicator getProgressIndicator()
1181   {
1182     return progressBar;
1183   }
1184
1185   protected void setProgressIndicator(IProgressIndicator pi)
1186   {
1187     progressBar = pi;
1188   }
1189
1190   protected void setProgressMessage(String message, long id)
1191   {
1192     if (progressBar != null)
1193     {
1194       progressBar.setProgressBar(message, id);
1195     }
1196   }
1197
1198   @Override
1199   public void showConsole(boolean show)
1200   {
1201     // default does nothing
1202   }
1203
1204   /**
1205    * Tries to fetch a PDB file and save to a temporary local file. Returns the
1206    * saved file path if successful, or null if not.
1207    * 
1208    * @param processingEntry
1209    * @return
1210    */
1211   protected String fetchPdbFile(PDBEntry processingEntry)
1212   {
1213     String filePath = null;
1214     Pdb pdbclient = new Pdb();
1215     AlignmentI pdbseq = null;
1216     String pdbid = processingEntry.getId();
1217     long handle = System.currentTimeMillis()
1218             + Thread.currentThread().hashCode();
1219   
1220     /*
1221      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
1222      */
1223     String msg = MessageManager.formatMessage("status.fetching_pdb",
1224             new Object[]
1225             { pdbid });
1226     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1227     // long hdl = startProgressBar(MessageManager.formatMessage(
1228     // "status.fetching_pdb", new Object[]
1229     // { pdbid }));
1230     try
1231     {
1232       pdbseq = pdbclient.getSequenceRecords(pdbid);
1233     } catch (Exception e)
1234     {
1235       System.err.println(
1236               "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
1237     } finally
1238     {
1239       msg = pdbid + " " + MessageManager.getString("label.state_completed");
1240       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1241       // stopProgressBar(msg, hdl);
1242     }
1243     /*
1244      * If PDB data were saved and are not invalid (empty alignment), return the
1245      * file path.
1246      */
1247     if (pdbseq != null && pdbseq.getHeight() > 0)
1248     {
1249       // just use the file name from the first sequence's first PDBEntry
1250       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
1251               .elementAt(0).getFile()).getAbsolutePath();
1252       processingEntry.setFile(filePath);
1253     }
1254     return filePath;
1255   }
1256
1257   /**
1258    * If supported, saves the state of the structure viewer to a temporary file
1259    * and returns the file, else returns null
1260    * 
1261    * @return
1262    */
1263   public File saveSession()
1264   {
1265     // TODO: a wait loop to ensure the file is written fully before returning?
1266     return getBinding() == null ? null : getBinding().saveSession();
1267   }
1268
1269   /**
1270    * Close down this instance of Jalview's Chimera viewer, giving the user the
1271    * option to close the associated Chimera window (process). They may wish to
1272    * keep it open until they have had an opportunity to save any work.
1273    * 
1274    * @param forceClose
1275    *          if true, close any linked Chimera process; if false, prompt first
1276    */
1277   @Override
1278   public void closeViewer(boolean forceClose)
1279   {
1280     AAStructureBindingModel binding = getBinding();
1281     if (binding != null && binding.isViewerRunning())
1282     {
1283       if (!forceClose)
1284       {
1285         String viewerName = getViewerName();
1286         String prompt = MessageManager
1287                 .formatMessage("label.confirm_close_viewer", new Object[]
1288                 { binding.getViewerTitle(viewerName, false), viewerName });
1289         prompt = JvSwingUtils.wrapTooltip(true, prompt);
1290         int confirm = JvOptionPane.showConfirmDialog(this, prompt,
1291                 MessageManager.getString("label.close_viewer"),
1292                 JvOptionPane.YES_NO_CANCEL_OPTION);
1293         /*
1294          * abort closure if user hits escape or Cancel
1295          */
1296         if (confirm == JvOptionPane.CANCEL_OPTION
1297                 || confirm == JvOptionPane.CLOSED_OPTION)
1298         {
1299           return;
1300         }
1301         forceClose = confirm == JvOptionPane.YES_OPTION;
1302       }
1303     }
1304     if (binding != null)
1305     {
1306       binding.closeViewer(forceClose);
1307     }
1308     setAlignmentPanel(null);
1309     _aps.clear();
1310     _alignwith.clear();
1311     _colourwith.clear();
1312     // TODO: check for memory leaks where instance isn't finalised because jmb
1313     // holds a reference to the window
1314     // jmb = null;
1315     dispose();
1316   }
1317
1318   @Override
1319   public void showHelp_actionPerformed()
1320   {
1321     try
1322     {
1323       String url = getBinding().getHelpURL();
1324       if (url != null)
1325       {
1326         BrowserLauncher.openURL(url);
1327       }
1328     } catch (IOException ex)
1329     {
1330       System.err
1331               .println("Show " + getViewerName() + " failed with: "
1332                       + ex.getMessage());
1333     }
1334   }
1335
1336 }