900252a6c5dd4aaeb1e7b27e0721f496cec6e83c
[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   void setChainMenuItems(List<String> chainNames)
517   {
518     chainMenu.removeAll();
519     if (chainNames == null || chainNames.isEmpty())
520     {
521       return;
522     }
523
524     /*
525      * add the 'All' menu item
526      */
527     JMenuItem menuItem = new JMenuItem(
528             MessageManager.getString("label.all"));
529     menuItem.addActionListener(new ActionListener()
530     {
531       @Override
532       public void actionPerformed(ActionEvent evt)
533       {
534         allChainsSelected = true;
535         for (int i = 0; i < chainMenu.getItemCount(); i++)
536         {
537           if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
538           {
539             ((JCheckBoxMenuItem) chainMenu.getItem(i)).setSelected(true);
540           }
541         }
542         showSelectedChains();
543         allChainsSelected = false;
544       }
545     });
546     chainMenu.add(menuItem);
547
548     /*
549      * add a menu item for each structure and chain
550      */
551     Collections.sort(chainNames);
552     for (String chain : chainNames)
553     {
554       String seqName = getSequenceNameForChain(chain);
555       if (seqName == null)
556       {
557         seqName = UNMAPPED;
558       }
559       int nameLength = seqName.length();
560       if (nameLength > 16)
561       {
562         seqName = seqName.substring(0, 8) + "..."
563                 + seqName.substring(nameLength - 8, nameLength);
564       }
565       String text = chain + " " + seqName;
566       menuItem = new JCheckBoxMenuItem(text, true);
567       menuItem.addItemListener(new ItemListener()
568       {
569         @Override
570         public void itemStateChanged(ItemEvent evt)
571         {
572           if (!allChainsSelected)
573           {
574             showSelectedChains();
575           }
576         }
577       });
578
579       chainMenu.add(menuItem);
580     }
581   }
582
583   /**
584    * Answers the name of the sequence mapped to the given chain (formatted as
585    * pdbId:chainId, e.g. 1A70:A). Answers null if no mapped sequence is found. If
586    * more than one sequence is matched, just answers the name of the first one
587    * found.
588    * 
589    * @param chain
590    * @return
591    */
592   private String getSequenceNameForChain(String chain)
593   {
594     String[] tokens = chain.split(":");
595     String pdbId = tokens[0];
596     String chainId = tokens[1];
597     List<StructureMapping> mappings = getBinding().getSsm()
598             .getMappingForChain(pdbId, chainId);
599     return mappings.isEmpty() ? null
600             : mappings.get(0).getSequence().getName();
601   }
602
603   /**
604    * Action on selecting one of Jalview's registered colour schemes
605    */
606   @Override
607   public void changeColour_actionPerformed(String colourSchemeName)
608   {
609     AlignmentI al = getAlignmentPanel().av.getAlignment();
610     ColourSchemeI cs = ColourSchemes.getInstance()
611             .getColourScheme(colourSchemeName, getAlignmentPanel().av, al,
612                     null);
613     getBinding().colourByJalviewColourScheme(cs);
614   }
615
616   /**
617    * Builds the colour menu
618    */
619   protected void buildColourMenu()
620   {
621     colourMenu.removeAll();
622     AlignmentI al = getAlignmentPanel().av.getAlignment();
623
624     /*
625      * add colour by sequence, by chain, by charge and cysteine
626      */
627     colourMenu.add(seqColour);
628     colourMenu.add(chainColour);
629     colourMenu.add(chargeColour);
630     chargeColour.setEnabled(!al.isNucleotide());
631
632     /*
633      * add all 'simple' (per-residue) colour schemes registered to Jalview
634      */
635     ButtonGroup itemGroup = ColourMenuHelper.addMenuItems(colourMenu, this,
636             al, true);
637
638     /*
639      * add 'colour by viewer' (menu item text is set in subclasses)
640      */
641     viewerColour.setSelected(false);
642     viewerColour.addActionListener(new ActionListener()
643     {
644       @Override
645       public void actionPerformed(ActionEvent actionEvent)
646       {
647         viewerColour_actionPerformed();
648       }
649     });
650     colourMenu.add(viewerColour);
651
652     /*
653      * add 'set background colour'
654      */
655     JMenuItem backGround = new JMenuItem();
656     backGround
657             .setText(MessageManager.getString("action.background_colour"));
658     backGround.addActionListener(new ActionListener()
659     {
660       @Override
661       public void actionPerformed(ActionEvent actionEvent)
662       {
663         background_actionPerformed();
664       }
665     });
666     colourMenu.add(backGround);
667
668     /*
669      * add colour buttons to a group so their selection is
670      * mutually exclusive (background colour is a separate option)
671      */
672     itemGroup.add(seqColour);
673     itemGroup.add(chainColour);
674     itemGroup.add(chargeColour);
675     itemGroup.add(viewerColour);
676   }
677
678   /**
679    * Construct menu items
680    */
681   protected void initMenus()
682   {
683     AAStructureBindingModel binding = getBinding();
684
685     seqColour = new JRadioButtonMenuItem();
686     seqColour.setText(MessageManager.getString("action.by_sequence"));
687     seqColour.setName(ViewerColour.BySequence.name());
688     seqColour.setSelected(binding.isColourBySequence());
689     seqColour.addActionListener(new ActionListener()
690     {
691       @Override
692       public void actionPerformed(ActionEvent actionEvent)
693       {
694         seqColour_actionPerformed();
695       }
696     });
697
698     chainColour = new JRadioButtonMenuItem();
699     chainColour.setText(MessageManager.getString("action.by_chain"));
700     chainColour.setName(ViewerColour.ByChain.name());
701     chainColour.addActionListener(new ActionListener()
702     {
703       @Override
704       public void actionPerformed(ActionEvent actionEvent)
705       {
706         chainColour_actionPerformed();
707       }
708     });
709
710     chargeColour = new JRadioButtonMenuItem();
711     chargeColour.setText(MessageManager.getString("label.charge_cysteine"));
712     chargeColour.setName(ViewerColour.ChargeCysteine.name());
713     chargeColour.addActionListener(new ActionListener()
714     {
715       @Override
716       public void actionPerformed(ActionEvent actionEvent)
717       {
718         chargeColour_actionPerformed();
719       }
720     });
721
722     viewerColour = new JRadioButtonMenuItem();
723     viewerColour
724             .setText(MessageManager.getString("label.colour_with_viewer"));
725     viewerColour.setToolTipText(MessageManager
726             .getString("label.let_viewer_manage_structure_colours"));
727     viewerColour.setName(ViewerColour.ByViewer.name());
728     viewerColour.setSelected(!binding.isColourBySequence());
729
730     if (_colourwith == null)
731     {
732       _colourwith = new Vector<>();
733     }
734     if (_alignwith == null)
735     {
736       _alignwith = new Vector<>();
737     }
738
739     ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
740             MessageManager.getString("label.colour_by"), this, _colourwith,
741             new ItemListener()
742             {
743               @Override
744               public void itemStateChanged(ItemEvent e)
745               {
746                 if (!seqColour.isSelected())
747                 {
748                   seqColour.doClick();
749                 }
750                 else
751                 {
752                   // update the viewer display now.
753                   seqColour_actionPerformed();
754                 }
755               }
756             });
757     viewMenu.add(seqColourBy);
758
759     showAlignmentOnly = new JCheckBoxMenuItem(
760             MessageManager.getString("label.show_alignment_only"));
761     showAlignmentOnly.addActionListener(new ActionListener()
762     {
763       @Override
764       public void actionPerformed(ActionEvent e)
765       {
766         hideHiddenRegions.setEnabled(showAlignmentOnly.isSelected());
767         getBinding().setShowAlignmentOnly(showAlignmentOnly.isSelected());
768         getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
769                 true);
770       }
771     });
772     viewMenu.add(showAlignmentOnly);
773
774     hideHiddenRegions = new JCheckBoxMenuItem(
775             MessageManager.getString("label.hide_hidden_regions"));
776     hideHiddenRegions.setEnabled(false);
777     hideHiddenRegions.addActionListener(new ActionListener()
778     {
779       @Override
780       public void actionPerformed(ActionEvent e)
781       {
782         getBinding().setHideHiddenRegions(hideHiddenRegions.isSelected());
783         getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
784                 false);
785       }
786     });
787     viewMenu.add(hideHiddenRegions);
788     viewMenu.add(fitToWindow);
789
790     final ItemListener handler = new ItemListener()
791     {
792       @Override
793       public void itemStateChanged(ItemEvent e)
794       {
795         if (_alignwith.isEmpty())
796         {
797           alignStructs.setEnabled(false);
798           alignStructs.setToolTipText(null);
799         }
800         else
801         {
802           alignStructs.setEnabled(true);
803           alignStructs.setToolTipText(MessageManager.formatMessage(
804                   "label.align_structures_using_linked_alignment_views",
805                   _alignwith.size()));
806         }
807       }
808     };
809     viewSelectionMenu = new ViewSelectionMenu(
810             MessageManager.getString("label.superpose_with"), this,
811             _alignwith, handler);
812     handler.itemStateChanged(null);
813     viewerActionMenu.add(viewSelectionMenu, 0);
814     viewerActionMenu.addMenuListener(new MenuListener()
815     {
816       @Override
817       public void menuSelected(MenuEvent e)
818       {
819         handler.itemStateChanged(null);
820       }
821
822       @Override
823       public void menuDeselected(MenuEvent e)
824       {
825       }
826
827       @Override
828       public void menuCanceled(MenuEvent e)
829       {
830       }
831     });
832
833     viewerActionMenu.setText(getViewerName());
834     helpItem.setText(MessageManager.formatMessage("label.viewer_help",
835             getViewerName()));
836
837     buildColourMenu();
838   }
839
840   /**
841    * Sends commands to the structure viewer to superimpose structures based on
842    * currently associated alignments. May optionally return an error message for
843    * the operation.
844    */
845   @Override
846   protected String alignStructsWithAllAlignPanels()
847   {
848     if (getAlignmentPanel() == null)
849     {
850       return null;
851     }
852
853     if (_alignwith.size() == 0)
854     {
855       _alignwith.add(getAlignmentPanel());
856     }
857
858     String reply = null;
859     try
860     {
861       reply = getBinding().superposeStructures(_alignwith);
862       if (reply != null && !reply.isEmpty())
863       {
864         String text = MessageManager
865                 .formatMessage("error.superposition_failed", reply);
866         statusBar.setText(text);
867       }
868     } catch (Exception e)
869     {
870       StringBuilder sb = new StringBuilder();
871       for (AlignmentViewPanel alignPanel : _alignwith)
872       {
873         sb.append("'").append(alignPanel.getViewName()).append("' ");
874       }
875       Cache.log.info("Couldn't align structures with the " + sb.toString()
876               + "associated alignment panels.", e);
877     }
878     return reply;
879   }
880
881   /**
882    * Opens a colour chooser dialog, and applies the chosen colour to the
883    * background of the structure viewer
884    */
885   @Override
886   public void background_actionPerformed()
887   {
888     String ttl = MessageManager.getString("label.select_background_colour");
889     ColourChooserListener listener = new ColourChooserListener()
890     {
891       @Override
892       public void colourSelected(Color c)
893       {
894         getBinding().setBackgroundColour(c);
895       }
896     };
897     JalviewColourChooser.showColourChooser(this, ttl, null, listener);
898   }
899
900   @Override
901   public void viewerColour_actionPerformed()
902   {
903     if (viewerColour.isSelected())
904     {
905       // disable automatic sequence colouring.
906       getBinding().setColourBySequence(false);
907     }
908   }
909
910   @Override
911   public void chainColour_actionPerformed()
912   {
913     chainColour.setSelected(true);
914     getBinding().colourByChain();
915   }
916
917   @Override
918   public void chargeColour_actionPerformed()
919   {
920     chargeColour.setSelected(true);
921     getBinding().colourByCharge();
922   }
923
924   @Override
925   public void seqColour_actionPerformed()
926   {
927     AAStructureBindingModel binding = getBinding();
928     binding.setColourBySequence(seqColour.isSelected());
929     if (_colourwith == null)
930     {
931       _colourwith = new Vector<>();
932     }
933     if (binding.isColourBySequence())
934     {
935       if (!binding.isLoadingFromArchive())
936       {
937         if (_colourwith.size() == 0 && getAlignmentPanel() != null)
938         {
939           // Make the currently displayed alignment panel the associated view
940           _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
941         }
942       }
943       // Set the colour using the current view for the associated alignframe
944       for (AlignmentViewPanel avp : _colourwith)
945       {
946         binding.updateStructureColours(avp);
947       }
948       seqColoursApplied = true;
949     }
950   }
951
952   @Override
953   public void pdbFile_actionPerformed()
954   {
955     // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
956     JalviewFileChooser chooser = new JalviewFileChooser(
957             Cache.getProperty("LAST_DIRECTORY"));
958
959     chooser.setFileView(new JalviewFileView());
960     chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
961     chooser.setToolTipText(MessageManager.getString("action.save"));
962
963     int value = chooser.showSaveDialog(this);
964
965     if (value == JalviewFileChooser.APPROVE_OPTION)
966     {
967       BufferedReader in = null;
968       try
969       {
970         // TODO: cope with multiple PDB files in view
971         in = new BufferedReader(
972                 new FileReader(getBinding().getStructureFiles()[0]));
973         File outFile = chooser.getSelectedFile();
974
975         PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
976         String data;
977         while ((data = in.readLine()) != null)
978         {
979           if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
980           {
981             out.println(data);
982           }
983         }
984         out.close();
985       } catch (Exception ex)
986       {
987         ex.printStackTrace();
988       } finally
989       {
990         if (in != null)
991         {
992           try
993           {
994             in.close();
995           } catch (IOException e)
996           {
997             // ignore
998           }
999         }
1000       }
1001     }
1002   }
1003
1004   @Override
1005   public void viewMapping_actionPerformed()
1006   {
1007     CutAndPasteTransfer cap = new CutAndPasteTransfer();
1008     try
1009     {
1010       cap.appendText(getBinding().printMappings());
1011     } catch (OutOfMemoryError e)
1012     {
1013       new OOMWarning(
1014               "composing sequence-structure alignments for display in text box.",
1015               e);
1016       cap.dispose();
1017       return;
1018     }
1019     Desktop.addInternalFrame(cap,
1020             MessageManager.getString("label.pdb_sequence_mapping"), 550,
1021             600);
1022   }
1023
1024   protected abstract String getViewerName();
1025
1026   /**
1027    * Configures the title and menu items of the viewer panel.
1028    */
1029   @Override
1030   public void updateTitleAndMenus()
1031   {
1032     AAStructureBindingModel binding = getBinding();
1033     if (binding.hasFileLoadingError())
1034     {
1035       repaint();
1036       return;
1037     }
1038     setChainMenuItems(binding.getChainNames());
1039
1040     this.setTitle(binding.getViewerTitle(getViewerName(), true));
1041
1042     /*
1043      * enable 'Superpose with' if more than one mapped structure
1044      */
1045     viewSelectionMenu.setEnabled(false);
1046     if (getBinding().getMappedStructureCount() > 1
1047             && getBinding().getSequence().length > 1)
1048     {
1049       viewSelectionMenu.setEnabled(true);
1050     }
1051
1052     /*
1053      * Show action menu if it has any enabled items
1054      */
1055     viewerActionMenu.setVisible(false);
1056     for (int i = 0; i < viewerActionMenu.getItemCount(); i++)
1057     {
1058       if (viewerActionMenu.getItem(i).isEnabled())
1059       {
1060         viewerActionMenu.setVisible(true);
1061         break;
1062       }
1063     }
1064
1065     if (!binding.isLoadingFromArchive())
1066     {
1067       seqColour_actionPerformed();
1068     }
1069   }
1070
1071   @Override
1072   public String toString()
1073   {
1074     return getTitle();
1075   }
1076
1077   @Override
1078   public boolean hasMapping()
1079   {
1080     if (worker != null && (addingStructures || _started))
1081     {
1082       return false;
1083     }
1084     if (getBinding() == null)
1085     {
1086       if (_aps == null || _aps.size() == 0)
1087       {
1088         // viewer has been closed, but we did at some point run.
1089         return true;
1090       }
1091       return false;
1092     }
1093     String[] pdbids = getBinding().getStructureFiles();
1094     if (pdbids == null)
1095     {
1096       return false;
1097     }
1098     int p=0;
1099     for (String pdbid:pdbids) {
1100       StructureMapping sm[] = getBinding().getSsm().getMapping(pdbid);
1101       if (sm!=null && sm.length>0 && sm[0]!=null) {
1102         p++;
1103       }
1104     }
1105     // only return true if there is a mapping for every structure file we have loaded
1106     if (p == 0 || p != pdbids.length)
1107     {
1108       return false;
1109     }
1110     // and that coloring has been applied
1111     return seqColoursApplied;
1112   }
1113
1114   @Override
1115   public void raiseViewer()
1116   {
1117     toFront();
1118   }
1119
1120   @Override
1121   public abstract AAStructureBindingModel getBinding();
1122
1123   /**
1124    * Show only the selected chain(s) in the viewer
1125    */
1126   protected void showSelectedChains()
1127   {
1128     setChainsToHide();
1129   
1130     /*
1131      * refresh display without resizing - easier to see what changed
1132      */
1133     getBinding().showStructures(getAlignmentPanel().getAlignViewport(),
1134             false);
1135   }
1136
1137   @Override
1138   public long startProgressBar(String msg)
1139   {
1140     // TODO would rather have startProgress/stopProgress as the
1141     // IProgressIndicator interface
1142     long tm = random.nextLong();
1143     if (progressBar != null)
1144     {
1145       progressBar.setProgressBar(msg, tm);
1146     }
1147     return tm;
1148   }
1149
1150   @Override
1151   public void stopProgressBar(String msg, long handle)
1152   {
1153     if (progressBar != null)
1154     {
1155       progressBar.setProgressBar(msg, handle);
1156     }
1157   }
1158
1159   protected IProgressIndicator getProgressIndicator()
1160   {
1161     return progressBar;
1162   }
1163
1164   protected void setProgressIndicator(IProgressIndicator pi)
1165   {
1166     progressBar = pi;
1167   }
1168
1169   protected void setProgressMessage(String message, long id)
1170   {
1171     if (progressBar != null)
1172     {
1173       progressBar.setProgressBar(message, id);
1174     }
1175   }
1176
1177   @Override
1178   public void showConsole(boolean show)
1179   {
1180     // default does nothing
1181   }
1182
1183   /**
1184    * Tries to fetch a PDB file and save to a temporary local file. Returns the
1185    * saved file path if successful, or null if not.
1186    * 
1187    * @param processingEntry
1188    * @return
1189    */
1190   protected String fetchPdbFile(PDBEntry processingEntry)
1191   {
1192     String filePath = null;
1193     Pdb pdbclient = new Pdb();
1194     AlignmentI pdbseq = null;
1195     String pdbid = processingEntry.getId();
1196     long handle = System.currentTimeMillis()
1197             + Thread.currentThread().hashCode();
1198   
1199     /*
1200      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
1201      */
1202     String msg = MessageManager.formatMessage("status.fetching_pdb",
1203             new Object[]
1204             { pdbid });
1205     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1206     // long hdl = startProgressBar(MessageManager.formatMessage(
1207     // "status.fetching_pdb", new Object[]
1208     // { pdbid }));
1209     try
1210     {
1211       pdbseq = pdbclient.getSequenceRecords(pdbid);
1212     } catch (Exception e)
1213     {
1214       System.err.println(
1215               "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
1216     } finally
1217     {
1218       msg = pdbid + " " + MessageManager.getString("label.state_completed");
1219       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1220       // stopProgressBar(msg, hdl);
1221     }
1222     /*
1223      * If PDB data were saved and are not invalid (empty alignment), return the
1224      * file path.
1225      */
1226     if (pdbseq != null && pdbseq.getHeight() > 0)
1227     {
1228       // just use the file name from the first sequence's first PDBEntry
1229       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
1230               .elementAt(0).getFile()).getAbsolutePath();
1231       processingEntry.setFile(filePath);
1232     }
1233     return filePath;
1234   }
1235
1236   /**
1237    * If supported, saves the state of the structure viewer to a temporary file
1238    * and returns the file, else returns null
1239    * 
1240    * @return
1241    */
1242   public File saveSession()
1243   {
1244     // TODO: a wait loop to ensure the file is written fully before returning?
1245     return getBinding() == null ? null : getBinding().saveSession();
1246   }
1247
1248   /**
1249    * Close down this instance of Jalview's Chimera viewer, giving the user the
1250    * option to close the associated Chimera window (process). They may wish to
1251    * keep it open until they have had an opportunity to save any work.
1252    * 
1253    * @param forceClose
1254    *          if true, close any linked Chimera process; if false, prompt first
1255    */
1256   @Override
1257   public void closeViewer(boolean forceClose)
1258   {
1259     AAStructureBindingModel binding = getBinding();
1260     if (binding != null && binding.isViewerRunning())
1261     {
1262       if (!forceClose)
1263       {
1264         String viewerName = getViewerName();
1265         String prompt = MessageManager
1266                 .formatMessage("label.confirm_close_viewer", new Object[]
1267                 { binding.getViewerTitle(viewerName, false), viewerName });
1268         prompt = JvSwingUtils.wrapTooltip(true, prompt);
1269         int confirm = JvOptionPane.showConfirmDialog(this, prompt,
1270                 MessageManager.getString("label.close_viewer"),
1271                 JvOptionPane.YES_NO_CANCEL_OPTION);
1272         /*
1273          * abort closure if user hits escape or Cancel
1274          */
1275         if (confirm == JvOptionPane.CANCEL_OPTION
1276                 || confirm == JvOptionPane.CLOSED_OPTION)
1277         {
1278           return;
1279         }
1280         forceClose = confirm == JvOptionPane.YES_OPTION;
1281       }
1282       binding.closeViewer(forceClose);
1283     }
1284     setAlignmentPanel(null);
1285     _aps.clear();
1286     _alignwith.clear();
1287     _colourwith.clear();
1288     // TODO: check for memory leaks where instance isn't finalised because jmb
1289     // holds a reference to the window
1290     // jmb = null;
1291     dispose();
1292   }
1293
1294   @Override
1295   public void showHelp_actionPerformed()
1296   {
1297     try
1298     {
1299       String url = getBinding().getHelpURL();
1300       if (url != null)
1301       {
1302         BrowserLauncher.openURL(url);
1303       }
1304     } catch (IOException ex)
1305     {
1306       System.err
1307               .println("Show " + getViewerName() + " failed with: "
1308                       + ex.getMessage());
1309     }
1310   }
1311
1312 }