JAL-3551 save structure viewer session refactorings, PyMol added
[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.JColorChooser;
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.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     // text is set in overrides of this method
681     viewerColour.setName(ViewerColour.ByViewer.name());
682     viewerColour.setSelected(!binding.isColourBySequence());
683
684     if (_colourwith == null)
685     {
686       _colourwith = new Vector<>();
687     }
688     if (_alignwith == null)
689     {
690       _alignwith = new Vector<>();
691     }
692
693     ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
694             MessageManager.getString("label.colour_by"), this, _colourwith,
695             new ItemListener()
696             {
697               @Override
698               public void itemStateChanged(ItemEvent e)
699               {
700                 if (!seqColour.isSelected())
701                 {
702                   seqColour.doClick();
703                 }
704                 else
705                 {
706                   // update the Chimera display now.
707                   seqColour_actionPerformed();
708                 }
709               }
710             });
711     viewMenu.add(seqColourBy);
712
713     final ItemListener handler = new ItemListener()
714     {
715       @Override
716       public void itemStateChanged(ItemEvent e)
717       {
718         alignStructs.setEnabled(!_alignwith.isEmpty());
719         alignStructs.setToolTipText(MessageManager.formatMessage(
720                 "label.align_structures_using_linked_alignment_views",
721                 _alignwith.size()));
722       }
723     };
724     viewSelectionMenu = new ViewSelectionMenu(
725             MessageManager.getString("label.superpose_with"), this,
726             _alignwith, handler);
727     handler.itemStateChanged(null);
728     viewerActionMenu.add(viewSelectionMenu, 0);
729     viewerActionMenu.addMenuListener(new MenuListener()
730     {
731       @Override
732       public void menuSelected(MenuEvent e)
733       {
734         handler.itemStateChanged(null);
735       }
736
737       @Override
738       public void menuDeselected(MenuEvent e)
739       {
740       }
741
742       @Override
743       public void menuCanceled(MenuEvent e)
744       {
745       }
746     });
747
748     buildColourMenu();
749   }
750
751   /**
752    * Sends commands to the structure viewer to superimpose structures based on
753    * currently associated alignments. May optionally return an error message for
754    * the operation.
755    */
756   @Override
757   protected String alignStructsWithAllAlignPanels()
758   {
759     if (getAlignmentPanel() == null)
760     {
761       return null;
762     }
763
764     if (_alignwith.size() == 0)
765     {
766       _alignwith.add(getAlignmentPanel());
767     }
768
769     String reply = null;
770     try
771     {
772       reply = getBinding().superposeStructures(_alignwith);
773       if (reply != null && !reply.isEmpty())
774       {
775         String text = MessageManager
776                 .formatMessage("error.superposition_failed", reply);
777         statusBar.setText(text);
778       }
779     } catch (Exception e)
780     {
781       StringBuffer sp = new StringBuffer();
782       for (AlignmentViewPanel alignPanel : _alignwith)
783       {
784         sp.append("'" + alignPanel.getViewName() + "' ");
785       }
786       Cache.log.info("Couldn't align structures with the " + sp.toString()
787               + "associated alignment panels.", e);
788     }
789     return reply;
790   }
791
792   @Override
793   public void background_actionPerformed()
794   {
795     Color col = JColorChooser.showDialog(this,
796             MessageManager.getString("label.select_background_colour"),
797             null);
798     if (col != null)
799     {
800       getBinding().setBackgroundColour(col);
801     }
802   }
803
804   @Override
805   public void viewerColour_actionPerformed()
806   {
807     if (viewerColour.isSelected())
808     {
809       // disable automatic sequence colouring.
810       getBinding().setColourBySequence(false);
811     }
812   }
813
814   @Override
815   public void chainColour_actionPerformed()
816   {
817     chainColour.setSelected(true);
818     getBinding().colourByChain();
819   }
820
821   @Override
822   public void chargeColour_actionPerformed()
823   {
824     chargeColour.setSelected(true);
825     getBinding().colourByCharge();
826   }
827
828   @Override
829   public void seqColour_actionPerformed()
830   {
831     AAStructureBindingModel binding = getBinding();
832     binding.setColourBySequence(seqColour.isSelected());
833     if (_colourwith == null)
834     {
835       _colourwith = new Vector<>();
836     }
837     if (binding.isColourBySequence())
838     {
839       if (!binding.isLoadingFromArchive())
840       {
841         if (_colourwith.size() == 0 && getAlignmentPanel() != null)
842         {
843           // Make the currently displayed alignment panel the associated view
844           _colourwith.add(getAlignmentPanel().alignFrame.alignPanel);
845         }
846       }
847       // Set the colour using the current view for the associated alignframe
848       for (AlignmentViewPanel alignPanel : _colourwith)
849       {
850         binding.colourBySequence(alignPanel);
851       }
852       seqColoursApplied = true;
853     }
854   }
855
856   @Override
857   public void pdbFile_actionPerformed()
858   {
859     JalviewFileChooser chooser = new JalviewFileChooser(
860             Cache.getProperty("LAST_DIRECTORY"));
861
862     chooser.setFileView(new JalviewFileView());
863     chooser.setDialogTitle(MessageManager.getString("label.save_pdb_file"));
864     chooser.setToolTipText(MessageManager.getString("action.save"));
865
866     int value = chooser.showSaveDialog(this);
867
868     if (value == JalviewFileChooser.APPROVE_OPTION)
869     {
870       BufferedReader in = null;
871       try
872       {
873         // TODO: cope with multiple PDB files in view
874         in = new BufferedReader(
875                 new FileReader(getBinding().getStructureFiles()[0]));
876         File outFile = chooser.getSelectedFile();
877
878         PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
879         String data;
880         while ((data = in.readLine()) != null)
881         {
882           if (!(data.indexOf("<PRE>") > -1 || data.indexOf("</PRE>") > -1))
883           {
884             out.println(data);
885           }
886         }
887         out.close();
888       } catch (Exception ex)
889       {
890         ex.printStackTrace();
891       } finally
892       {
893         if (in != null)
894         {
895           try
896           {
897             in.close();
898           } catch (IOException e)
899           {
900             // ignore
901           }
902         }
903       }
904     }
905   }
906
907   @Override
908   public void viewMapping_actionPerformed()
909   {
910     CutAndPasteTransfer cap = new CutAndPasteTransfer();
911     try
912     {
913       cap.appendText(getBinding().printMappings());
914     } catch (OutOfMemoryError e)
915     {
916       new OOMWarning(
917               "composing sequence-structure alignments for display in text box.",
918               e);
919       cap.dispose();
920       return;
921     }
922     Desktop.addInternalFrame(cap,
923             MessageManager.getString("label.pdb_sequence_mapping"), 550,
924             600);
925   }
926
927   protected abstract String getViewerName();
928
929   /**
930    * Configures the title and menu items of the viewer panel.
931    */
932   @Override
933   public void updateTitleAndMenus()
934   {
935     AAStructureBindingModel binding = getBinding();
936     if (binding.hasFileLoadingError())
937     {
938       repaint();
939       return;
940     }
941     setChainMenuItems(binding.getChainNames());
942
943     this.setTitle(binding.getViewerTitle(getViewerName(), true));
944
945     /*
946      * enable 'Superpose with' if more than one mapped structure
947      */
948     viewSelectionMenu.setEnabled(false);
949     if (getBinding().getStructureFiles().length > 1
950             && getBinding().getSequence().length > 1)
951     {
952       viewSelectionMenu.setEnabled(true);
953     }
954
955     /*
956      * Show action menu if it has any enabled items
957      */
958     viewerActionMenu.setVisible(false);
959     for (int i = 0; i < viewerActionMenu.getItemCount(); i++)
960     {
961       if (viewerActionMenu.getItem(i).isEnabled())
962       {
963         viewerActionMenu.setVisible(true);
964         break;
965       }
966     }
967
968     if (!binding.isLoadingFromArchive())
969     {
970       seqColour_actionPerformed();
971     }
972   }
973
974   @Override
975   public String toString()
976   {
977     return getTitle();
978   }
979
980   @Override
981   public boolean hasMapping()
982   {
983     if (worker != null && (addingStructures || _started))
984     {
985       return false;
986     }
987     if (getBinding() == null)
988     {
989       if (_aps == null || _aps.size() == 0)
990       {
991         // viewer has been closed, but we did at some point run.
992         return true;
993       }
994       return false;
995     }
996     String[] pdbids = getBinding().getStructureFiles();
997     if (pdbids == null)
998     {
999       return false;
1000     }
1001     int p=0;
1002     for (String pdbid:pdbids) {
1003       StructureMapping sm[] = getBinding().getSsm().getMapping(pdbid);
1004       if (sm!=null && sm.length>0 && sm[0]!=null) {
1005         p++;
1006       }
1007     }
1008     // only return true if there is a mapping for every structure file we have loaded
1009     if (p == 0 || p != pdbids.length)
1010     {
1011       return false;
1012     }
1013     // and that coloring has been applied
1014     return seqColoursApplied;
1015   }
1016
1017   @Override
1018   public void raiseViewer()
1019   {
1020     toFront();
1021   }
1022
1023   @Override
1024   public long startProgressBar(String msg)
1025   {
1026     // TODO would rather have startProgress/stopProgress as the
1027     // IProgressIndicator interface
1028     long tm = random.nextLong();
1029     if (progressBar != null)
1030     {
1031       progressBar.setProgressBar(msg, tm);
1032     }
1033     return tm;
1034   }
1035
1036   @Override
1037   public void stopProgressBar(String msg, long handle)
1038   {
1039     if (progressBar != null)
1040     {
1041       progressBar.setProgressBar(msg, handle);
1042     }
1043   }
1044
1045   protected IProgressIndicator getProgressIndicator()
1046   {
1047     return progressBar;
1048   }
1049
1050   protected void setProgressIndicator(IProgressIndicator pi)
1051   {
1052     progressBar = pi;
1053   }
1054
1055   protected void setProgressMessage(String message, long id)
1056   {
1057     if (progressBar != null)
1058     {
1059       progressBar.setProgressBar(message, id);
1060     }
1061   }
1062
1063   @Override
1064   public void showConsole(boolean show)
1065   {
1066     // default does nothing
1067   }
1068
1069   /**
1070    * Show only the selected chain(s) in the viewer
1071    */
1072   protected void showSelectedChains()
1073   {
1074     List<String> toshow = new ArrayList<>();
1075     for (int i = 0; i < chainMenu.getItemCount(); i++)
1076     {
1077       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
1078       {
1079         JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
1080         if (item.isSelected())
1081         {
1082           toshow.add(item.getText());
1083         }
1084       }
1085     }
1086     getBinding().showChains(toshow);
1087   }
1088
1089   /**
1090    * Tries to fetch a PDB file and save to a temporary local file. Returns the
1091    * saved file path if successful, or null if not.
1092    * 
1093    * @param processingEntry
1094    * @return
1095    */
1096   protected String fetchPdbFile(PDBEntry processingEntry)
1097   {
1098     String filePath = null;
1099     Pdb pdbclient = new Pdb();
1100     AlignmentI pdbseq = null;
1101     String pdbid = processingEntry.getId();
1102     long handle = System.currentTimeMillis()
1103             + Thread.currentThread().hashCode();
1104   
1105     /*
1106      * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
1107      */
1108     String msg = MessageManager.formatMessage("status.fetching_pdb",
1109             new Object[]
1110             { pdbid });
1111     getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1112     // long hdl = startProgressBar(MessageManager.formatMessage(
1113     // "status.fetching_pdb", new Object[]
1114     // { pdbid }));
1115     try
1116     {
1117       pdbseq = pdbclient.getSequenceRecords(pdbid);
1118     } catch (Exception e)
1119     {
1120       System.err.println(
1121               "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
1122     } finally
1123     {
1124       msg = pdbid + " " + MessageManager.getString("label.state_completed");
1125       getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
1126       // stopProgressBar(msg, hdl);
1127     }
1128     /*
1129      * If PDB data were saved and are not invalid (empty alignment), return the
1130      * file path.
1131      */
1132     if (pdbseq != null && pdbseq.getHeight() > 0)
1133     {
1134       // just use the file name from the first sequence's first PDBEntry
1135       filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
1136               .elementAt(0).getFile()).getAbsolutePath();
1137       processingEntry.setFile(filePath);
1138     }
1139     return filePath;
1140   }
1141
1142   /**
1143    * If supported, saves the state of the structure viewer to a temporary file
1144    * and returns the file, else returns null
1145    * 
1146    * @return
1147    */
1148   public File saveSession()
1149   {
1150     // TODO: a wait loop to ensure the file is written fully before returning?
1151     return getBinding() == null ? null : getBinding().saveSession();
1152   }
1153
1154 }