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